diff --git a/.env.example b/.env.example deleted file mode 100644 index 4e66c7e6..00000000 --- a/.env.example +++ /dev/null @@ -1,6 +0,0 @@ -NEXT_PUBLIC_APP_URL=http://localhost:3000 -NEXT_PUBLIC_STORYBOOK_URL=http://localhost:6006 -NEXT_PUBLIC_SUPABASE_URL= -NEXT_PUBLIC_SUPABASE_ANON_KEY= -NEXT_PUBLIC_POSTHOG_KEY= -NEXT_PUBLIC_POSTHOG_HOST= \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/new-component-ticket.md b/.github/ISSUE_TEMPLATE/new-component-ticket.md index bcee02b8..42df9d9a 100644 --- a/.github/ISSUE_TEMPLATE/new-component-ticket.md +++ b/.github/ISSUE_TEMPLATE/new-component-ticket.md @@ -3,25 +3,28 @@ name: New Component Ticket [HACKTOBERFEST] about: Create a new animated component task for Hacktoberfest title: Component Name labels: hacktoberfest -assignees: '' - +assignees: "" --- ### Description + Create the component as shown in the video attached below. ### Animation Preview + - Screenshot/Video: [Upload file here] - > [!NOTE] > Please refer to the **Animata contributing guidelines** available [here](https://www.animata.design/docs/contributing) for rules on how to contribute to the project. Let us know in the comments if you’re working on this issue, and feel free to ask any questions! ### Requirements + 1. Create a new animated component that matches the provided design. 2. Add example usage of the component in the documentation or storybook. +3. **Add credits** for any resources, references, or assets used, as specified in the **Additional Resources** section. +
@@ -36,12 +39,14 @@ Create the component as shown in the video attached below. > [!IMPORTANT] > To ensure more contributors have the opportunity to participate, we kindly request that: +> > - Each contributor submits only **one pull request** related to this issue. > - If you're interested in working on this issue, please comment below. We will assign the issue on a **first-come, first-served basis**. --- ### Additional Resources + --- diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 9e6fb9da..ace6ab08 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -43,12 +43,21 @@ jobs: NEXT_PUBLIC_POSTHOG_HOST: ${{ vars.NEXT_PUBLIC_POSTHOG_HOST }} run: yarn build - name: Deploy to Cloudflare Pages - uses: cloudflare/pages-action@v1 + id: deploy + uses: cloudflare/wrangler-action@v3 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - projectName: animata - directory: out + command: pages deploy out --project-name=animata --branch=dev-${{ github.event.pull_request.head.ref }} gitHubToken: ${{ secrets.GITHUB_TOKEN }} - branch: ${{ github.event.pull_request.head.ref }} - wranglerVersion: "3" + - name: Comment Deployment URL + uses: actions/github-script@v6 + with: + github-token: ${{secrets.GITHUB_TOKEN}} + script: | + await github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: `🚀 Preview deployed to: ${{ steps.deploy.outputs.deployment-url }}` + }) diff --git a/animata/accordion/faq.stories.tsx b/animata/accordion/faq.stories.tsx new file mode 100644 index 00000000..9d4d6d56 --- /dev/null +++ b/animata/accordion/faq.stories.tsx @@ -0,0 +1,53 @@ +import Faq from "@/animata/accordion/faq"; +import { Meta, StoryObj } from "@storybook/react"; + +const faqData = [ + { + id: 1, + question: "How late does the internet close?", + answer: "The internet doesn't close. It's available 24/7.", + icon: "❤️", + iconPosition: "right", + }, + { + id: 2, + question: "Do I need a license to browse this website?", + answer: "No, you don't need a license to browse this website.", + }, + { + id: 3, + question: "What flavour are the cookies?", + answer: "Our cookies are digital, not edible. They're used for website functionality.", + }, + { + id: 4, + question: "Can I get lost here?", + answer: "Yes, but we do have a return policy", + icon: "⭐", + iconPostion: "left", + }, + { + id: 5, + question: "What if I click the wrong button?", + answer: "Don't worry, you can always go back or refresh the page.", + }, +]; + +const meta = { + title: "Accordion/Faq", + component: Faq, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], + argTypes: {}, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + args: { + data: faqData, + }, +}; diff --git a/animata/accordion/faq.tsx b/animata/accordion/faq.tsx new file mode 100644 index 00000000..845f1c23 --- /dev/null +++ b/animata/accordion/faq.tsx @@ -0,0 +1,126 @@ +"use client"; + +import React, { useState } from "react"; +import { motion } from "framer-motion"; + +import * as Accordion from "@radix-ui/react-accordion"; + +interface FAQItem { + id: number; + question: string; + answer: string; + icon?: string; + iconPosition?: string; +} + +interface FaqSectionProps { + data: FAQItem[]; +} + +export default function FaqSection({ data }: FaqSectionProps) { + const [openItem, setOpenItem] = useState(null); + + return ( +
+
Every day, 9:01 AM
+ + setOpenItem(value)} + > + {data.map((item) => ( + + + +
+ {item.icon && ( + + {item.icon} + + )} + {item.question} +
+ + + {openItem === item.id.toString() ? ( + + + + ) : ( + + + + )} + +
+
+ + +
+
+ {item.answer} +
+
+
+
+
+
+ ))} +
+
+ ); +} diff --git a/animata/button/animated-follow-button.stories.tsx b/animata/button/animated-follow-button.stories.tsx new file mode 100644 index 00000000..6c163f74 --- /dev/null +++ b/animata/button/animated-follow-button.stories.tsx @@ -0,0 +1,72 @@ +import AnimatedFollowButton from "@/animata/button/animated-follow-button"; +import { Meta, StoryObj } from "@storybook/react"; + +const meta: Meta = { + title: "Button/Animated Follow Button", + component: AnimatedFollowButton, + parameters: { layout: "centered" }, + argTypes: { + initialText: { control: "text" }, + changeText: { control: "text" }, + className: { control: "text" }, + changeTextClassName: { control: "text" }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + args: { + initialText: "Follow", + changeText: "Following!", + className: "h-16 bg-green-100 text-green-700 flex rounded-full items-center justify-center", + changeTextClassName: "h-16 bg-green-700 text-green-100 rounded-full text-white flex items-center justify-center", + }, + render: (args) => ( +
+ +
+ ), +}; + +export const DifferentAnimations: Story = { + args: {}, + render: () => { + const buttons = [ + { + initialText: "Default", + changeText: "Up To Down", + animationType: "up-to-down" as const, + color: "blue", + }, + { + initialText: "Click Me", + changeText: "Down To Up", + animationType: "down-to-up" as const, + color: "zinc", + }, + { + initialText: "Click Me", + changeText: "Zoom In", + animationType: "zoom-in" as const, + color: "red", + }, + ]; + + return ( +
+ {buttons.map(({ initialText, changeText, animationType, color }, idx) => ( + {initialText}} + changeText={{changeText}} + animationType={animationType} + /> + ))} +
+ ); + }, +}; diff --git a/animata/button/animated-follow-button.tsx b/animata/button/animated-follow-button.tsx new file mode 100644 index 00000000..8ee8c638 --- /dev/null +++ b/animata/button/animated-follow-button.tsx @@ -0,0 +1,87 @@ +"use client"; + +import React, { useState } from "react"; +import { AnimatePresence, motion } from "framer-motion"; + +interface AnimatedFollowButtonProps { + initialText: React.ReactElement | string; // Text or element displayed initially + changeText: React.ReactElement | string; // Text or element displayed after the button is clicked + className?: string; // ClassName prop for custom button styling + changeTextClassName?: string; // ClassName prop for custom styling of changeText + animationType?: "up-to-down" | "down-to-up" | "left-to-right" | "right-to-left" | "zoom-in" | "zoom-out"; // Prop to define animation type +} + +const AnimatedFollowButton: React.FC = ({ + initialText, + changeText, + className, + changeTextClassName, + animationType = "up-to-down", // Set default animation to "up-to-down" +}) => { + const [isClicked, setIsClicked] = useState(false); // Track button click state + + // Determine animation settings based on animationType prop + const getAnimation = () => { + switch (animationType) { + case "down-to-up": + return { initial: { y: 20 }, animate: { y: 0 }, exit: { y: 20 } }; // Down to up animation + case "left-to-right": + return { initial: { x: -20 }, animate: { x: 0 }, exit: { x: -20 } }; // Left to right animation + case "right-to-left": + return { initial: { x: 20 }, animate: { x: 0 }, exit: { x: 20 } }; // Right to left animation + case "zoom-in": + return { initial: { scale: 0.8 }, animate: { scale: 1 }, exit: { scale: 0.8 } }; // Zoom in animation + case "zoom-out": + return { initial: { scale: 1.2 }, animate: { scale: 1 }, exit: { scale: 1.2 } }; // Zoom out animation + case "up-to-down": + default: + return { initial: { y: -20 }, animate: { y: 0 }, exit: { y: -20 } }; // Default: Up to down animation + } + }; + + const animation = getAnimation(); // Get animation settings based on the selected type + + return ( + + {isClicked ? ( + // Button after being clicked + setIsClicked(false)} // On click, toggle button state + initial={{ opacity: 0 }} // Initial animation for opacity + animate={{ opacity: 1 }} // Animate to full opacity + exit={{ opacity: 0 }} // Exit animation for opacity + > + {/* Change text with defined animation */} + + {changeText} {/* Display the changeText */} + + + ) : ( + // Button before being clicked + setIsClicked(true)} // On click, toggle button state + initial={{ opacity: 0 }} // Initial animation for opacity + animate={{ opacity: 1 }} // Animate to full opacity + exit={{ opacity: 0 }} // Exit animation for opacity + > + {/* Initial text with defined animation */} + + {initialText} {/* Display the initialText */} + + + )} + + ); +}; + +export default AnimatedFollowButton; diff --git a/animata/button/ripple-button.stories.tsx b/animata/button/ripple-button.stories.tsx new file mode 100644 index 00000000..42c1c430 --- /dev/null +++ b/animata/button/ripple-button.stories.tsx @@ -0,0 +1,24 @@ +import RippleButton from "@/animata/button/ripple-button"; +import { Meta, StoryObj } from "@storybook/react"; + +const meta = { + title: "Button/Ripple Button", + component: RippleButton, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], + argTypes: { + children: { control: "text" }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Homepage: Story = { + args: { + children: "Homepage", + }, +}; diff --git a/animata/button/ripple-button.tsx b/animata/button/ripple-button.tsx new file mode 100644 index 00000000..7f1493c4 --- /dev/null +++ b/animata/button/ripple-button.tsx @@ -0,0 +1,127 @@ +"use client"; +import { useCallback, useRef, useState } from "react"; + +interface RippleButtonProps extends React.ButtonHTMLAttributes { + children: React.ReactNode; +} + +export default function RippleButton({ children, ...props }: RippleButtonProps) { + const buttonRef = useRef(null); + const rippleRef = useRef(null); + const [isHovered, setIsHovered] = useState(false); + + const createRipple = useCallback( + (event: React.MouseEvent) => { + if (isHovered || !buttonRef.current || !rippleRef.current) return; + setIsHovered(true); + + const button = buttonRef.current; + const ripple = rippleRef.current; + const rect = button.getBoundingClientRect(); + const size = Math.max(rect.width, rect.height) * 2; + const x = event.clientX - rect.left - size / 2; + const y = event.clientY - rect.top - size / 2; + + ripple.style.width = `${size}px`; + ripple.style.height = `${size}px`; + ripple.style.left = `${x}px`; + ripple.style.top = `${y}px`; + + ripple.classList.remove("ripple-leave"); + ripple.classList.add("ripple-enter"); + }, + [isHovered], + ); + + const removeRipple = useCallback((event: React.MouseEvent) => { + if (event.target !== event.currentTarget) return; + if (!buttonRef.current || !rippleRef.current) return; + setIsHovered(false); + + const button = buttonRef.current; + const ripple = rippleRef.current; + const rect = button.getBoundingClientRect(); + const size = Math.max(rect.width, rect.height) * 2; + const x = event.clientX - rect.left - size / 2; + const y = event.clientY - rect.top - size / 2; + + ripple.style.left = `${x}px`; + ripple.style.top = `${y}px`; + + ripple.classList.remove("ripple-enter"); + ripple.classList.add("ripple-leave"); + + const handleAnimationEnd = () => { + if (ripple) { + ripple.classList.remove("ripple-leave"); + ripple.removeEventListener("animationend", handleAnimationEnd); + } + }; + + ripple.addEventListener("animationend", handleAnimationEnd); + }, []); + + const handleMouseMove = useCallback( + (event: React.MouseEvent) => { + if (!buttonRef.current || !rippleRef.current || !isHovered) return; + + const button = buttonRef.current; + const ripple = rippleRef.current; + const rect = button.getBoundingClientRect(); + const size = Math.max(rect.width, rect.height) * 2; + const x = event.clientX - rect.left - size / 2; + const y = event.clientY - rect.top - size / 2; + + ripple.style.left = `${x}px`; + ripple.style.top = `${y}px`; + }, + [isHovered], + ); + + return ( + + ); +} diff --git a/animata/button/slide-arrow-button.stories.tsx b/animata/button/slide-arrow-button.stories.tsx new file mode 100644 index 00000000..b5681a70 --- /dev/null +++ b/animata/button/slide-arrow-button.stories.tsx @@ -0,0 +1,22 @@ +import SlideArrowButton from "@/animata/button/slide-arrow-button"; +import { Meta, StoryObj } from "@storybook/react"; + +const meta = { + title: "Button/Slide Arrow Button", + component: SlideArrowButton, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], + argTypes: {}, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + args: { + text: "Get Started", + primaryColor: "#6f3cff", + }, +}; diff --git a/animata/button/slide-arrow-button.tsx b/animata/button/slide-arrow-button.tsx new file mode 100644 index 00000000..5ccac720 --- /dev/null +++ b/animata/button/slide-arrow-button.tsx @@ -0,0 +1,33 @@ +import React from "react"; +import { ArrowRight } from "lucide-react"; + +interface SlideArrowButtonProps extends React.ButtonHTMLAttributes { + text?: string; + primaryColor?: string; +} + +export default function SlideArrowButton({ + text = "Get Started", + primaryColor = "#6f3cff", + className = "", + ...props +}: SlideArrowButtonProps) { + return ( + + ); +} diff --git a/animata/card/WebHooks-card.tsx b/animata/card/WebHooks-card.tsx new file mode 100644 index 00000000..317fe3d6 --- /dev/null +++ b/animata/card/WebHooks-card.tsx @@ -0,0 +1,53 @@ +import React, { useState } from "react"; +interface webHooksCardCommentProps { + leftBoxElem: string; + rightBoxElem: string; +} + +export const WebHooks = ({ leftBoxElem, rightBoxElem }: webHooksCardCommentProps) => { + const [isHovered, setIsHovered] = useState(false); + + return ( + <> +
+
+ {/* Left Box */} +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + {leftBoxElem} +
+ + {/* Connecting Line */} +
+ + {/* Animated Ball */} +
+ + {/* Right Box */} +
+ {rightBoxElem} +
+
+
+ + ); +}; diff --git a/animata/card/case-study-card.stories.tsx b/animata/card/case-study-card.stories.tsx new file mode 100644 index 00000000..54df0431 --- /dev/null +++ b/animata/card/case-study-card.stories.tsx @@ -0,0 +1,37 @@ +import CaseStudyCard from "@/animata/card/case-study-card"; +import { Meta, StoryObj } from "@storybook/react"; + +const meta = { + title: "Card/Case Study Card", + component: CaseStudyCard, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], + argTypes: {}, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + args: { + title: + "How Delivery Hero streamlines marketing reports across all their brands with Clarisights", + category: "BOOKS", + logo: "https://plus.unsplash.com/premium_photo-1686593923007-218c4db786ca?q=80&w=3095&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", + link: "https://github.com/codse/animata", + type: "content", + image: + "https://images.unsplash.com/photo-1675285410608-ddd6bb430b19?q=80&w=2938&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", + }, +}; + +export const Secondary: Story = { + args: { + image: + "https://images.unsplash.com/photo-1675285410608-ddd6bb430b19?q=80&w=2938&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", + link: "https://github.com/codse/animata", + type: "simple-image", + }, +}; diff --git a/animata/card/case-study-card.tsx b/animata/card/case-study-card.tsx new file mode 100644 index 00000000..f0c03814 --- /dev/null +++ b/animata/card/case-study-card.tsx @@ -0,0 +1,111 @@ +import React from "react"; + +import { cn } from "@/lib/utils"; + +interface CaseStudyCardProps extends React.HTMLAttributes { + title?: string; + category?: string; + image?: string; + logo?: string; + link?: string; + type?: "content" | "simple-image"; // Decides between text or image +} + +// ContentCard Component for rendering text + image +const ContentCard: React.FC = ({ title, category, image, logo }) => { + return ( +
+ {image &&
} + +
+ {category &&
{category}
} + + {title && ( +
+ {title} +
+ )} +
+ {logo && ( // Check if image exists + {title} + )} +
+ ); +}; + +// SimpleImageCard component for rendering only image +const SimpleImageCard: React.FC = ({ image }) => { + return ( +
+ ); +}; + +const HoverRevealSlip = ({ show }: { show: React.ReactNode }) => { + const common = "absolute flex w-full h-full [backface-visibility:hidden]"; + + return ( +
+ {/* Back cover - static */} +
+ + {/* Card container with slight book opening effect on hover */} +
+ {/* Front side of the card */} +
{show}
+
+ + {/* Sliding link/tab coming out from behind */} +
+
CLICK TO READ
+
+
+ ); +}; + +// Main CaseStudyCard Component +export default function CaseStudyCard({ + title, + category, + link, + image, + logo, + type, +}: CaseStudyCardProps) { + return ( + + ); +} diff --git a/animata/card/comment-reply-card.stories.tsx b/animata/card/comment-reply-card.stories.tsx new file mode 100644 index 00000000..e2a1736b --- /dev/null +++ b/animata/card/comment-reply-card.stories.tsx @@ -0,0 +1,29 @@ +import CommentReplyCard from "@/animata/card/comment-reply-card"; +import { Meta, StoryObj } from "@storybook/react"; + +const meta = { + title: "Card/Comment Reply Card", + component: CommentReplyCard, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], + argTypes: {}, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const initialComments = [ + { + id: 1, + user: "Mike", + text: ["Is it just me, or is the font size on this page designed for ants?"], + time: "13 hours ago", + avatarColor: "#e8824b", + }, +]; + +export const Primary: Story = { + args: { initialComments }, +}; diff --git a/animata/card/comment-reply-card.tsx b/animata/card/comment-reply-card.tsx new file mode 100644 index 00000000..815f682a --- /dev/null +++ b/animata/card/comment-reply-card.tsx @@ -0,0 +1,220 @@ +"use client"; + +import React, { useEffect, useRef, useState } from "react"; +import { AnimatePresence, motion } from "framer-motion"; +import { Check, X } from "lucide-react"; + +interface Comment { + id: number; + user: string; + text: string[]; + time: string; + avatarColor: string; +} + +const containerVariants = { + hidden: { height: "auto" }, + visible: { height: "auto", transition: { duration: 0.5, ease: "easeInOut" } }, +}; + +const commentVariants = { + hidden: { opacity: 0, y: 20 }, + visible: { opacity: 1, y: 0, transition: { duration: 0.3 } }, + exit: { opacity: 0, y: -20, transition: { duration: 0.3 } }, +}; + +export default function CommentReplyCard({ initialComments }: { initialComments: Comment[] }) { + const [comments, setComments] = useState([...initialComments]); + const [newComment, setNewComment] = useState(""); + const [isAnimating, setIsAnimating] = useState(false); + const containerRef = useRef(null); + const inputRef = useRef(null); + + const handleAddComment = () => { + if (newComment.trim() === "") return; + setIsAnimating(true); + const newCommentData: Comment = { + id: comments.length + 1, + user: "Emily", + text: [newComment], + time: "now", + avatarColor: "#e84b9d", + }; + + setTimeout(() => { + setComments((prevComments) => [...prevComments, newCommentData]); + setNewComment(""); + setTimeout(() => { + setIsAnimating(false); + if (inputRef.current) { + inputRef.current.focus(); + } + }, 300); + }, 300); + }; + + useEffect(() => { + if (containerRef.current) { + containerRef.current.scrollTop = containerRef.current.scrollHeight; + } + }, [comments]); + + return ( +
+
+
+
+ + Comment +
+
+ + +
+
+ + {/* Comment Section */} + + + + {comments.map((comment) => ( + +
+
+ + + +
+
+ {comment.user} + {comment.time} +
+
+
+ {comment.text.map((text, textIndex) => ( + + {text} + + ))} +
+
+ ))} +
+
+
+
+ + {/* Input Box */} + + {!isAnimating && ( + +
+
+ + + +
+ setNewComment(e.target.value)} + onKeyPress={(e) => { + if (e.key === "Enter") { + handleAddComment(); + } + }} + /> + +
+
+ )} +
+
+ ); +} + +const CommentIcon: React.FC = () => { + return ( + + + + ); +}; diff --git a/animata/card/fluid-tabs.stories.tsx b/animata/card/fluid-tabs.stories.tsx new file mode 100644 index 00000000..6f814c98 --- /dev/null +++ b/animata/card/fluid-tabs.stories.tsx @@ -0,0 +1,19 @@ +import FluidTabs from "@/animata/card/fluid-tabs"; +import { Meta, StoryObj } from "@storybook/react"; + +const meta = { + title: "Tabs/Fluid Tabs", + component: FluidTabs, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], + argTypes: {}, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + args: {}, +}; diff --git a/animata/card/fluid-tabs.tsx b/animata/card/fluid-tabs.tsx new file mode 100644 index 00000000..b4b03b99 --- /dev/null +++ b/animata/card/fluid-tabs.tsx @@ -0,0 +1,82 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { AnimatePresence, motion } from "framer-motion"; +import { Inbox, Landmark, PieChart } from "lucide-react"; + +const tabs = [ + { + id: "accounts", + label: "Accounts", + icon: , + }, + { + id: "deposits", + label: "Deposits", + icon: , + }, + { + id: "funds", + label: "Funds", + icon: , + }, +]; + +export default function FluidTabs() { + const [activeTab, setActiveTab] = useState("funds"); + const [touchedTab, setTouchedTab] = useState(null); + const [prevActiveTab, setPrevActiveTab] = useState("funds"); + const timeoutRef = useRef(null); + + useEffect(() => { + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }; + }, []); + + const handleTabClick = (tabId: string) => { + setPrevActiveTab(activeTab); + setActiveTab(tabId); + setTouchedTab(tabId); + + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + timeoutRef.current = setTimeout(() => { + setTouchedTab(null); + }, 300); + }; + + const getTabIndex = (tabId: string) => tabs.findIndex((tab) => tab.id === tabId); + + return ( +
+
+ + + + {tabs.map((tab) => ( + handleTabClick(tab.id)} + > + {tab.icon} + {tab.label} + + ))} +
+
+ ); +} diff --git a/animata/card/integration-pills.stories.tsx b/animata/card/integration-pills.stories.tsx new file mode 100644 index 00000000..8745fc66 --- /dev/null +++ b/animata/card/integration-pills.stories.tsx @@ -0,0 +1,19 @@ +import IntegrationPills from "@/animata/card/integration-pills"; +import { Meta, StoryObj } from "@storybook/react"; + +const meta = { + title: "Card/Integration pills", + component: IntegrationPills, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], + argTypes: {}, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + args: {}, +}; diff --git a/animata/card/integration-pills.tsx b/animata/card/integration-pills.tsx new file mode 100644 index 00000000..2f65fb95 --- /dev/null +++ b/animata/card/integration-pills.tsx @@ -0,0 +1,91 @@ +import React from "react"; + +import { cn } from "@/lib/utils"; + +const brands = [ + { + name: "Shopify", + className: "bg-gray-200", + hoverClass: "group-hover:scale-75 group-hover:text-gray-500", + }, + { + name: "RSS", + className: "bg-gray-200 font-bold", + hoverClass: + "group-hover:border-blue-500 group-hover:text-blue-500 group-hover:bg-white group-hover:border-2", + }, + { + name: "Zapier", + className: "bg-gray-200", + hoverClass: "group-hover:scale-75 group-hover:text-gray-500", + }, + { + name: "Slack", + className: "bg-gray-200", + hoverClass: "group-hover:scale-75 group-hover:text-gray-500", + }, + { + name: "Webflow", + className: "bg-gray-200", + hoverClass: "group-hover:scale-75 group-hover:text-gray-500", + }, + { + name: "Squarespace", + className: "bg-gray-200", + hoverClass: "group-hover:scale-75 group-hover:text-gray-500", + }, + { + name: "Twitter", + className: "bg-gray-200 font-bold", + hoverClass: + "group-hover:border-green-500 group-hover:text-green-500 group-hover:bg-white group-hover:border-2", + }, + { + name: "TikTok", + className: "bg-gray-200", + hoverClass: "group-hover:scale-75 group-hover:text-gray-500", + }, + { + name: "n8n", + className: "bg-gray-200", + hoverClass: "group-hover:scale-75 group-hover:text-gray-500", + }, + { + name: "BuySellAds", + className: "bg-gray-200", + hoverClass: "group-hover:scale-75 group-hover:text-gray-500", + }, + { + name: "Mastodon", + className: "bg-gray-200 font-bold", + hoverClass: + "group-hover:border-purple-500 group-hover:text-purple-500 group-hover:bg-white group-hover:border-2", + }, + { + name: "Gumroad", + className: "bg-gray-200", + hoverClass: "group-hover:scale-75 group-hover:text-gray-500", + }, +]; + +export default function IntegrationPills() { + return ( +
+ {/* Rectangular box around all cards */} +
+ {brands.map((brand, index) => ( +
+ {brand.name} +
+ ))} +
+
+ ); +} diff --git a/animata/card/notice-card.stories.tsx b/animata/card/notice-card.stories.tsx new file mode 100644 index 00000000..fcb2b02d --- /dev/null +++ b/animata/card/notice-card.stories.tsx @@ -0,0 +1,35 @@ +import NoticeCard from "@/animata/card/notice-card"; +import { Meta, StoryObj } from "@storybook/react"; + +const meta = { + title: "Card/Notice Card", + component: NoticeCard, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], + argTypes: { + acceptText: { control: "text" }, + title: { control: "text" }, + description: { control: "text" }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + args: { + acceptText: "Accept", + title: "To your attention!", + description: + "Due to severe weather conditions, we will be closed from 11th to 14th of January.", + }, + render: (args) => { + return ( + <> + + + ); + }, +}; diff --git a/animata/card/notice-card.tsx b/animata/card/notice-card.tsx new file mode 100644 index 00000000..92d798d7 --- /dev/null +++ b/animata/card/notice-card.tsx @@ -0,0 +1,114 @@ +import { useState } from "react"; +import { motion } from "framer-motion"; + +import { cn } from "@/lib/utils"; + +interface NoticeCardProps { + acceptText?: string; + title?: string; + description?: string; +} + +export default function NoticeCard({ + acceptText = "Accept", + title = "To your attention!", + description = "Due to severe weather conditions, we will be closed from 11th to 14th of January.", +}: NoticeCardProps) { + const [isAccepted, setIsAccepted] = useState(false); + + const handleClick = () => { + setIsAccepted(!isAccepted); + }; + + const bgClass = isAccepted + ? "bg-green-300" + : "bg-gradient-to-r from-slate-50 via-slate-50 to-green-100"; + + return ( +
+ {/* Outer container with breathing scaling effect */} +
+ {/* Mid-level static container */} + + + {/* Stable inner content */} +
+
+ {/* Icon */} +
+ + + +
+ + {/* Title */} +

{title}

+ + {/* Description */} +

{description}

+ + {/* Toggle Button */} +
+ {/* Toggle Handle */} +
+ + {/* Accept Text */} + + + + + {acceptText} + +
+
+
+
+
+ ); +} diff --git a/animata/card/notification-card.stories.tsx b/animata/card/notification-card.stories.tsx new file mode 100644 index 00000000..ebb90c07 --- /dev/null +++ b/animata/card/notification-card.stories.tsx @@ -0,0 +1,44 @@ +import NotificationCard from "@/animata/card/notification-card"; +import { Meta, StoryObj } from "@storybook/react"; + +const meta = { + title: "Card/Notification Card", + component: NotificationCard, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], + argTypes: {}, +} satisfies Meta; + +export default meta; +type Story = StoryObj; +const RosettaLogo: React.FC = () => ( + + + + R + + +); + +export const Primary: Story = { + args: { + title: "Rosetta AI", + message: "Your dataset on renewable energy efficiency has just been cited by Dr. A. Scott", + RosettaLogo, + userInfo: { + name: "Dr. A. Scott", + title: "Senior Researcher", + avatar: "https://avatars.githubusercontent.com/u/17984567?v=4", + }, + }, +}; diff --git a/animata/card/notification-card.tsx b/animata/card/notification-card.tsx new file mode 100644 index 00000000..d0237ae2 --- /dev/null +++ b/animata/card/notification-card.tsx @@ -0,0 +1,66 @@ +import React from "react"; +import { motion } from "framer-motion"; + +interface UserInfoProps { + name: string; + title: string; + avatar: string; +} + +interface NotificationCardProps { + title: string; + message: string; + userInfo: UserInfoProps; + RosettaLogo: React.FC; +} + +const NotificationCard: React.FC = ({ + title, + message, + userInfo, + RosettaLogo, +}) => { + return ( +
+ {/* Notification Section */} + +
+ +

{title}

+
+
+

{message}

+
+
+ + {/* User Info Section */} +
+ +
+ {userInfo.name} +
+

{userInfo.name}

+

{userInfo.title}

+
+
+
+
+
+ ); +}; + +export default NotificationCard; diff --git a/animata/card/notify-user-info.stories.tsx b/animata/card/notify-user-info.stories.tsx new file mode 100644 index 00000000..32894144 --- /dev/null +++ b/animata/card/notify-user-info.stories.tsx @@ -0,0 +1,26 @@ +import NotifyUserInfo from "@/animata/card/notify-user-info"; +import { Meta, StoryObj } from "@storybook/react"; + +const meta = { + title: "Card/Notify User Info", + component: NotifyUserInfo, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], + argTypes: {}, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + args: { + aiName: "Rostra AI", + userName: "Sandra", + paperTopic: "neural conditions", + doctorName: "DoctorLLM", + earnings: "$0.25c", + weekTotal: "$400", + }, +}; diff --git a/animata/card/notify-user-info.tsx b/animata/card/notify-user-info.tsx new file mode 100644 index 00000000..b3cd9a11 --- /dev/null +++ b/animata/card/notify-user-info.tsx @@ -0,0 +1,123 @@ +"use client"; +import React from "react"; +import { motion } from "framer-motion"; + +interface NotificationCardProps { + aiName?: string; + userName?: string; + paperTopic?: string; + doctorName?: string; + earnings?: string; + weekTotal?: string; +} + +export default function NotifyUserInfo({ + aiName = "AI name", + userName = "User", + paperTopic = "general topic", + doctorName = "AI Doctor", + earnings = "$0.20c", + weekTotal = "$400", +}: NotificationCardProps) { + return ( + +
+ {/* Animated Header */} + +
+
+ {aiName[0]} +
+
+ + {aiName} +
+ +
+ {/* Vertical Line */} + + + {/* Animated Content Wrapper */} + + {/* Animated Content */} + +

+ Hey {userName}, your paper on {paperTopic} was used by{" "} + + {doctorName} + {" "} + to give an assessment to a patient today. +

+
+ + {/* Earnings Notification */} + +
+

+ You've earned {earnings} because a new doctor LLM used your knowledge. This + week's total: {weekTotal}. +

+
+
+
+
+
+
+ ); +} diff --git a/animata/card/reminder-scheduler.stories.tsx b/animata/card/reminder-scheduler.stories.tsx new file mode 100644 index 00000000..e67ce647 --- /dev/null +++ b/animata/card/reminder-scheduler.stories.tsx @@ -0,0 +1,49 @@ +import { useState } from "react"; + +import ReminderScheduler from "@/animata/card/reminder-scheduler"; +import { Meta, StoryObj } from "@storybook/react"; + +const meta = { + title: "Card/Reminder Scheduler", + component: ReminderScheduler, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], + argTypes: { + isRepeating: { control: "boolean" }, + repeatInterval: { control: "text" }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + render: (args) => { + const [isRepeating, setIsRepeating] = useState(args.isRepeating); + const [repeatInterval, setRepeatInterval] = useState(args.repeatInterval); + + const toggleRepeating = () => { + setIsRepeating((prev) => !prev); + }; + + return ( + + ); + }, + args: { + isRepeating: true, + repeatInterval: "Weekly", + toggleRepeating: () => {}, + setRepeatInterval: () => {}, + daysOfWeek: ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"], + }, +}; diff --git a/animata/card/reminder-scheduler.tsx b/animata/card/reminder-scheduler.tsx new file mode 100644 index 00000000..94aee3df --- /dev/null +++ b/animata/card/reminder-scheduler.tsx @@ -0,0 +1,144 @@ +import React, { useEffect, useState } from "react"; + +import { cn } from "@/lib/utils"; + +interface ReminderSchedulerProps { + isRepeating: boolean; + toggleRepeating: () => void; + repeatInterval: string; + setRepeatInterval: (interval: string) => void; + daysOfWeek: string[]; +} + +const ReminderScheduler: React.FC = ({ + isRepeating, + toggleRepeating, + repeatInterval, + setRepeatInterval, + daysOfWeek, +}) => { + const selectedDays = isRepeating ? new Set(["Th", "Fr", "Su"]) : new Set(["Mo", "We", "Sa"]); + return ( +
+ {/* Toggle Switch */} +
+ Is repeating + +
+ + {/* Repeat Interval Dropdown */} +
+ + +
+ + {/* Day Selection */} +
+
+ {daysOfWeek.map((day) => ( + + ))} +
+
+
+ ); +}; + +const Switch = ({ toggle, value }: { toggle: () => void; value: boolean }) => { + return ( + + ); +}; +// credit: author: harimanok_ , https://github.com/hari +interface SwapTextProps extends React.ComponentPropsWithoutRef<"div"> { + initialText: string; + finalText: string; + supportsHover?: boolean; + textClassName?: string; + initialTextClassName?: string; + finalTextClassName?: string; + disableClick?: boolean; + check?: boolean; +} + +function SwapText({ + initialText, + finalText, + className, + supportsHover = true, + textClassName, + initialTextClassName, + finalTextClassName, + disableClick, + check, + ...props +}: SwapTextProps) { + const [active, setActive] = useState(!check); + useEffect(() => { + let timeoutId: NodeJS.Timeout; + if (check) { + timeoutId = setTimeout(() => { + setActive((current) => !current); + }, 100); + } else { + timeoutId = setTimeout(() => { + setActive((current) => !current); + }, 100); + } + return () => { + clearTimeout(timeoutId); // clear the timeout when component unmounts + }; + }, [check]); + const common = "block transition-all duration-1000 ease-slow"; + const longWord = finalText.length > initialText.length ? finalText : null; + return ( +
+
!disableClick && setActive((current) => !current)} + > + + {initialText} + {Boolean(longWord?.length) && {longWord}} + + + {finalText} + +
+
+ ); +} +export default ReminderScheduler; diff --git a/animata/card/score-card.stories.tsx b/animata/card/score-card.stories.tsx new file mode 100644 index 00000000..234ae686 --- /dev/null +++ b/animata/card/score-card.stories.tsx @@ -0,0 +1,35 @@ +import Scorecard from "@/animata/card/score-card"; +import { Meta, StoryObj } from "@storybook/react"; + +const meta = { + title: "Card/Score Card", + component: Scorecard, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], + argTypes: {}, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const homeTeam = { + name: "Brentford", + logo: "https://upload.wikimedia.org/wikipedia/en/2/2a/Brentford_FC_crest.svg", +}; + +const awayTeam = { + name: "Man. City", + logo: "https://upload.wikimedia.org/wikipedia/en/e/eb/Manchester_City_FC_badge.svg", +}; +export const Primary: Story = { + args: { + homeTeam: homeTeam, + awayTeam: awayTeam, + homeScore: 1, + awayScore: 2, + matchTime: "2nd Half - 70'", + scorer: "Foden", + }, +}; diff --git a/animata/card/score-card.tsx b/animata/card/score-card.tsx new file mode 100644 index 00000000..65b9d826 --- /dev/null +++ b/animata/card/score-card.tsx @@ -0,0 +1,121 @@ +import React, { useState } from "react"; +import { motion } from "framer-motion"; + +interface Team { + name: string; + logo: string; +} + +interface ScoreProps { + homeTeam: Team; + awayTeam: Team; + homeScore: number; + awayScore: number; + matchTime: string; + scorer: string; +} + +export default function LiveScoreWidget({ + homeTeam, + awayTeam, + homeScore, + awayScore, + matchTime, + scorer, +}: ScoreProps) { + const [scored, setScored] = useState(false); + const [awScore, setAwScore] = useState(awayScore); + const [popAnimation, setPopAnimation] = useState(false); + + return ( +
+ +
+ {/* Home Team */} +
+ {homeTeam.name} + {!scored && {homeTeam.name}} +
+ + {/* Score */} +
+ setPopAnimation(false)} + > + {homeScore}-{awScore} + +
{matchTime}
+
+ + {/* Away Team */} +
+ {awayTeam.name} + {!scored && {awayTeam.name}} +
+
+ {scored && ( + +
+
+ + {scorer} scores! +
+ now +
+
+ )} +
+ + +
+ ); +} + +function FootbalIcon() { + return ( + + + + + ); +} diff --git a/animata/card/subscribe-card.stories.tsx b/animata/card/subscribe-card.stories.tsx new file mode 100644 index 00000000..ee2f016a --- /dev/null +++ b/animata/card/subscribe-card.stories.tsx @@ -0,0 +1,19 @@ +import SubscribeCard from "@/animata/card/subscribe-card"; +import { Meta, StoryObj } from "@storybook/react"; + +const meta = { + title: "Card/Subscribe Card", + component: SubscribeCard, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], + argTypes: {}, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + args: {}, +}; diff --git a/animata/card/subscribe-card.tsx b/animata/card/subscribe-card.tsx new file mode 100644 index 00000000..d8bb5d35 --- /dev/null +++ b/animata/card/subscribe-card.tsx @@ -0,0 +1,32 @@ +import { Check } from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; + +interface SubscribeCardProps { + title?: string; + placeholder?: string; + buttonText?: string; +} + +export default function SubscribeCard({ + title = "Want to read the rest?", + placeholder = "justin@buttondown.email", + buttonText = "Subscribe for $5/mo", +}: SubscribeCardProps) { + return ( +
+

{title}

+
+ + +
+
+ ); +} diff --git a/animata/card/survey-card.stories.tsx b/animata/card/survey-card.stories.tsx new file mode 100644 index 00000000..b5bb1727 --- /dev/null +++ b/animata/card/survey-card.stories.tsx @@ -0,0 +1,49 @@ +import SurveyCard from "@/animata/card/survey-card"; +import { Meta, StoryObj } from "@storybook/react"; + +const meta = { + title: "Card/Survey Card", + component: SurveyCard, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], + argTypes: {}, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + args: { + items: [ + { + vote: 50, + itemName: "Charmander", + }, + { + vote: 60, + itemName: "Pikachu", + }, + { + vote: 20, + itemName: "Squirtle", + }, + ], + width: 250, // Fixed width + surveyTitle: "Pokemon Survey ?", + }, + render: (args) => { + return ( + <> + Survey Card +
+ {/* Adjust width to be fixed and control growth by width */} +
+ +
+
+ + ); + }, +}; diff --git a/animata/card/survey-card.tsx b/animata/card/survey-card.tsx new file mode 100644 index 00000000..8e086a49 --- /dev/null +++ b/animata/card/survey-card.tsx @@ -0,0 +1,88 @@ +import { useEffect, useMemo, useRef, useState } from "react"; + +import { cn } from "@/lib/utils"; + +interface SurveyCardProps { + /** + * The items to display in the Survey. + * Each item should have a vote and itemName. + */ + items: { + vote: number; + itemName?: string; + }[]; + /** + * The width of the Survey. recommended to use with more than 250px + */ + width?: number; + /** + * The title of the Survey. + */ + surveyTitle?: string; +} + +export default function SurveyCard({ items, width: providedWidth, surveyTitle }: SurveyCardProps) { + const [{ width }, setSize] = useState({ + width: providedWidth ?? 250, + }); + // Calculate total votes and max votes using useMemo + const totalVotes = useMemo(() => items.reduce((acc, item) => acc + item.vote, 0), [items]); + const maxVote = useMemo(() => Math.max(...items.map((item) => item.vote)), [items]); + + const containerRef = useRef(null); + + useEffect(() => { + setSize({ + width: providedWidth ?? containerRef.current?.offsetWidth ?? 250, + }); + }, [providedWidth, items]); + + const [isParentHovered, setIsParentHovered] = useState(false); + + const handleParentMouseOver = () => { + setIsParentHovered(true); + }; + + const handleParentMouseOut = () => { + setIsParentHovered(false); + }; + + return ( +
+
+

{surveyTitle}

+
+ {items.map((item, index) => { + const clampedProgress = Math.max(0, item.vote); + // saving overflow over itemName using 8/12 + const barWidth = isParentHovered ? (clampedProgress / totalVotes) * 100 * (8 / 12) : 30; + return ( +
+
+
+ {item.itemName} +
+
+ ); + })} +
+ ); +} diff --git a/animata/card/webhooks-card.stories.tsx b/animata/card/webhooks-card.stories.tsx new file mode 100644 index 00000000..290d2127 --- /dev/null +++ b/animata/card/webhooks-card.stories.tsx @@ -0,0 +1,20 @@ +import { WebHooks } from "@/animata/card/WebHooks-card"; +import { Meta, StoryObj } from "@storybook/react"; + +const meta = { + title: "Card/Web Hooks", + component: WebHooks, + parameters: {}, + tags: ["autodocs"], + argTypes: {}, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + args: { + leftBoxElem: "Button Down ", + rightBoxElem: "Your app", + }, +}; diff --git a/animata/container/animated-dock.stories.tsx b/animata/container/animated-dock.stories.tsx new file mode 100644 index 00000000..705d1df4 --- /dev/null +++ b/animata/container/animated-dock.stories.tsx @@ -0,0 +1,88 @@ +import { Meta, StoryObj } from "@storybook/react"; +import { Home, Search, Bell, User } from "lucide-react"; +import AnimatedDock from "@/animata/container/animated-dock"; + + +const meta = { + title: "Container/Animated Dock", + component: AnimatedDock, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], + argTypes: { + largeClassName: { control: "text" }, + smallClassName: { control: "text" }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + + +// Example contents for AnimatedDock +const dockItems = [ + { title: "Home", icon: , href: "/" }, + { title: "Search", icon: , href: "/search" }, + { title: "Notifications", icon: , href: "/notifications" }, + { title: "Profile", icon: , href: "/profile" }, +]; + + +// Primary story for AnimatedDock (default layout) +export const Primary: Story = { + args: { + items: dockItems, + largeClassName: "max-w-lg", + smallClassName: "w-full", + }, + render: (args) => ( +
+ +
+ ), +}; + + +// Story focused on the Small layout (for mobile view) +export const Small: Story = { + args: { + items: dockItems, + smallClassName: "w-full", + }, + render: (args) => ( +
+ +
+ ), +}; + + +// Story focused on the Large layout (for desktop view) +export const Large: Story = { + args: { + items: dockItems, + largeClassName: "max-w-lg", + }, + render: (args) => ( +
+ +
+ ), +}; + + +// Story showing both layouts at the same time (for comparison) +export const Multiple: Story = { + args: { + items: dockItems, + largeClassName: "max-w-lg", + smallClassName: "w-full", + }, + render: (args) => ( +
+ + +
+ ), +}; diff --git a/animata/container/animated-dock.tsx b/animata/container/animated-dock.tsx new file mode 100644 index 00000000..190815cc --- /dev/null +++ b/animata/container/animated-dock.tsx @@ -0,0 +1,186 @@ +import { cn } from "@/lib/utils"; // Import utility for conditional class names +import { + AnimatePresence, // Enables animation presence detection + MotionValue, // Type for motion values + motion, // Main component for animations + useMotionValue, // Hook to create a motion value + useSpring, // Hook to create smooth spring animations + useTransform, // Hook to transform motion values +} from "framer-motion"; +import Link from "next/link"; // Next.js Link component for navigation +import React, { useRef, useState } from "react"; // Importing React hooks +import { Menu, X } from "lucide-react"; // Importing icons from lucide-react + +// Interface for props accepted by the AnimatedDock component +interface AnimatedDockProps { + items: { title: string; icon: React.ReactNode; href: string }[]; // Array of menu items + largeClassName?: string; // Optional class name for large dock + smallClassName?: string; // Optional class name for small dock +} + +// Main AnimatedDock component that renders both LargeDock and SmallDock +export default function AnimatedDock({ items, largeClassName, smallClassName }: AnimatedDockProps) { + return ( + <> + {/* Render LargeDock for larger screens */} + + {/* Render SmallDock for smaller screens */} + + + ); +} + +// Component for the large dock, visible on larger screens +const LargeDock = ({ + items, + className, +}: { + items: { title: string; icon: React.ReactNode; href: string }[]; // Items to display + className?: string; // Optional class name +}) => { + const mouseXPosition = useMotionValue(Infinity); // Create a motion value for mouse X position + return ( + mouseXPosition.set(e.pageX)} // Update mouse X position on mouse move + onMouseLeave={() => mouseXPosition.set(Infinity)} // Reset on mouse leave + className={cn( + "mx-auto hidden h-16 items-end gap-4 rounded-2xl bg-white/10 px-4 pb-3 dark:bg-black/10 md:flex", // Large dock styles + className, + "border border-gray-200/30 backdrop-blur-sm dark:border-gray-800/30", + )} + > + {/* Render each dock icon */} + {items.map((item) => ( + + ))} + + ); +}; + +// Component for individual icons in the dock +function DockIcon({ + mouseX, + title, + icon, + href, +}: { + mouseX: MotionValue; // Motion value for mouse position + title: string; // Title of the icon + icon: React.ReactNode; // Icon component + href: string; // Link destination +}) { + const ref = useRef(null); // Ref for measuring distance from mouse + + // Calculate the distance from the mouse to the icon + const distanceFromMouse = useTransform(mouseX, (val) => { + const bounds = ref.current?.getBoundingClientRect() ?? { x: 0, width: 0 }; // Get icon bounds + return val - bounds.x - bounds.width / 2; // Calculate distance from center + }); + + // Transform properties for width and height based on mouse distance + const widthTransform = useTransform(distanceFromMouse, [-150, 0, 150], [40, 80, 40]); + const heightTransform = useTransform(distanceFromMouse, [-150, 0, 150], [40, 80, 40]); + + // Transform properties for icon size based on mouse distance + const iconWidthTransform = useTransform(distanceFromMouse, [-150, 0, 150], [20, 40, 20]); + const iconHeightTransform = useTransform(distanceFromMouse, [-150, 0, 150], [20, 40, 20]); + + // Spring animations for smooth transitions + const width = useSpring(widthTransform, { mass: 0.1, stiffness: 150, damping: 12 }); + const height = useSpring(heightTransform, { mass: 0.1, stiffness: 150, damping: 12 }); + const iconWidth = useSpring(iconWidthTransform, { mass: 0.1, stiffness: 150, damping: 12 }); + const iconHeight = useSpring(iconHeightTransform, { mass: 0.1, stiffness: 150, damping: 12 }); + + const [isHovered, setIsHovered] = useState(false); // State for hover effect + + return ( + + setIsHovered(true)} // Handle mouse enter + onMouseLeave={() => setIsHovered(false)} // Handle mouse leave + className="relative flex aspect-square items-center justify-center rounded-full bg-white/20 text-black shadow-lg backdrop-blur-md dark:bg-black/20 dark:text-white" + > + + {/* Tooltip that appears on hover */} + {isHovered && ( + + {title} {/* Tooltip text */} + + )} + + + {icon} {/* Render the icon */} + + + + ); +} + +// Component for the small dock, visible on smaller screens +const SmallDock = ({ + items, + className, +}: { + items: { title: string; icon: React.ReactNode; href: string }[]; // Items to display + className?: string; // Optional class name +}) => { + const [isOpen, setIsOpen] = useState(false); // State to manage open/close of the small dock + + return ( +
+ + {/* Render menu items when open */} + {isOpen && ( + + {items.map((item, index) => ( + + +
{item.icon}
{/* Render the icon */} + +
+ ))} +
+ )} +
+ {/* Button to toggle the small dock open/close */} + +
+ ); +}; diff --git a/animata/fabs/speed-dial.stories.tsx b/animata/fabs/speed-dial.stories.tsx new file mode 100644 index 00000000..dff05b85 --- /dev/null +++ b/animata/fabs/speed-dial.stories.tsx @@ -0,0 +1,57 @@ +import React from "react"; +import { Copy, Edit, Share2, Trash } from "lucide-react"; + +import SpeedDial from "@/animata/fabs/speed-dial"; +import { Meta, StoryObj } from "@storybook/react"; + +const meta = { + title: "Fabs/Speed Dial", + component: SpeedDial, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], + argTypes: { + direction: { + control: { type: "select" }, + options: ["up", "down", "left", "right"], + description: "Direction of the SpeedDial", + table: { + type: { summary: "string" }, + defaultValue: { summary: "right" }, + }, + }, + actionButtons: { + description: + "Array of action buttons to be displayed in the SpeedDial. Each button should have an icon, label, and action function.", + table: { + type: { summary: "Array of objects" }, + }, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const actionButtons = [ + { icon: , label: "Copy", key: "copy", action: () => console.log("Copy clicked") }, + { icon: , label: "Edit", key: "edit", action: () => console.log("Edit clicked") }, + { icon: , label: "Share", key: "share", action: () => console.log("Share clicked") }, + { icon: , label: "Delete", key: "delete", action: () => console.log("Delete clicked") }, +]; + +// Primary story for SpeedDial +export const Primary: Story = { + args: { + direction: "right", + actionButtons: actionButtons, + }, +}; + +export const Secondary: Story = { + args: { + direction: "down", + actionButtons: actionButtons, + }, +}; diff --git a/animata/fabs/speed-dial.tsx b/animata/fabs/speed-dial.tsx new file mode 100644 index 00000000..fdd00a83 --- /dev/null +++ b/animata/fabs/speed-dial.tsx @@ -0,0 +1,107 @@ +"use client"; + +import React, { useState } from "react"; +import { Plus } from "lucide-react"; + +interface SpeedialProps { + direction: string; + actionButtons: Array<{ + icon: React.ReactNode; + label: string; + key: string; + action: () => void; + }>; +} + +interface TooltipProps { + text: string; + children: React.ReactNode; + direction: string; +} + +const Tooltip: React.FC = ({ text, children, direction }) => { + const [visible, setVisible] = useState(false); + + const showTooltip = () => setVisible(true); + const hideTooltip = () => setVisible(false); + + return ( +
+ {children} + {visible && ( +
+ {text} +
+ )} +
+ ); +}; + +export default function Speeddial({ direction, actionButtons }: SpeedialProps) { + const [isHovered, setIsHovered] = useState(false); + + const getAnimation = () => { + switch (direction) { + case "up": + return "origin-bottom flex-col order-0"; + case "down": + return "origin-top flex-col order-2"; + case "left": + return "origin-right order-0"; + case "right": + return "origin-left order-2"; + default: + return ""; + } + }; + + const handleMouseEnter = () => setIsHovered(true); + const handleMouseLeave = () => setIsHovered(false); + + const getGlassyClasses = () => { + return "backdrop-filter backdrop-blur-xl bg-white border border-white rounded-xl shadow-lg transition-all duration-300"; + }; + + //customize your action buttons here + + return ( +
+ + + {/* Speed Dial Actions */} +
+ {actionButtons.map((action, index) => ( + + + + ))} +
+
+ ); +} diff --git a/animata/feature-cards/confirmation-message.stories.tsx b/animata/feature-cards/confirmation-message.stories.tsx new file mode 100644 index 00000000..9edda5e1 --- /dev/null +++ b/animata/feature-cards/confirmation-message.stories.tsx @@ -0,0 +1,24 @@ +import ConfirmationMessage from "@/animata/feature-cards/confirmation-message"; +import { Meta, StoryObj } from "@storybook/react"; + +const meta = { + title: "Feature Cards/Confirmation Message", + component: ConfirmationMessage, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], + argTypes: {}, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + args: { + successMessage: "Process Successful", + labelName: "Animata", + labelMessage: `The Confirmation Message component is a sleek, animated UI element that displays a checkmark with a success message. + It expands to reveal a personalized detailed description of the process.`, + }, +}; diff --git a/animata/feature-cards/confirmation-message.tsx b/animata/feature-cards/confirmation-message.tsx new file mode 100644 index 00000000..d1860cb2 --- /dev/null +++ b/animata/feature-cards/confirmation-message.tsx @@ -0,0 +1,109 @@ +import { motion } from "framer-motion"; + +import { cn } from "@/lib/utils"; + +interface ConfirmationMessageProps { + /** + * The message to appear in green box when the process will be successfully completed. + */ + successMessage: string; + + /** + * The name of the organization/bot who performs the operations. + */ + labelName: string; + + /** + * The brief about the process/text/output. + */ + labelMessage: string; + + /** + * Class name for the background element. + */ + backgroundClassName?: string; + + /** + * Class name for the container element. + */ + containerClassName?: string; +} + +export default function ConfirmationMessage({ + successMessage = "Process Successful", + labelName = "Animata", + labelMessage, + backgroundClassName, + containerClassName, +}: ConfirmationMessageProps) { + return ( +
+
+ + {/* Parent Container for message */} +
+
+ {/* Checkmark */} +
+ {" "} + {/* Adjusted size */} + ✓ +
+ + {/* Expanding green box with sliding text */} + + + {successMessage} + + +
+ + {/* Container to control height animation */} + + {/* Message box */} +
+
+ {labelName[0]} +
+
+

{labelName}

+ + {labelMessage.length > 200 ? labelMessage.slice(0, 199) + "..." : labelMessage} + +
+
+
+
+
+ ); +} diff --git a/animata/feature-cards/content-scan.stories.tsx b/animata/feature-cards/content-scan.stories.tsx new file mode 100644 index 00000000..97b5cdd8 --- /dev/null +++ b/animata/feature-cards/content-scan.stories.tsx @@ -0,0 +1,34 @@ +import ContentScan from "@/animata/feature-cards/content-scan"; +import { Meta, StoryObj } from "@storybook/react"; + +const meta = { + title: "Feature Cards/Content Scan", + component: ContentScan, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], + argTypes: {}, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + args: { + content: + "Ten years ago there were only five private prisons in the country, with a population of 2,000 inmates; now, 30 there are 100, with 62,000 inmates. It is expected that by the coming decade, the number will hit 360,000 according to reports. The private contracting of prisoners for work fosters.", + highlightWords: [ + "Ten years ago", + "only five private prisons", + "now, 30", + "62,000", + "are 100", + "62\\,000 inmates", + "expected", + "coming decade", + ], + scanDuration: 4, + reverseDuration: 1, + }, +}; diff --git a/animata/feature-cards/content-scan.tsx b/animata/feature-cards/content-scan.tsx new file mode 100644 index 00000000..041cef70 --- /dev/null +++ b/animata/feature-cards/content-scan.tsx @@ -0,0 +1,249 @@ +import React, { useEffect, useRef, useState } from "react"; +import { motion, useAnimation } from "framer-motion"; + +interface ContentScannerProps { + content: string; + highlightWords: string[]; + scanDuration?: number; + reverseDuration?: number; +} + +const ContentScanner: React.FC = ({ + content, + highlightWords, + scanDuration = 3, + reverseDuration = 1, +}) => { + const [scanning, setScanning] = useState(false); + const [aiProbability, setAiProbability] = useState(0); + const containerRef = useRef(null); + const scannerRef = useRef(null); + const contentRef = useRef(null); + const scannerAnimation = useAnimation(); + const [highlightedWords, setHighlightedWords] = useState([]); + const [animationPhase, setAnimationPhase] = useState<"idle" | "forward" | "paused" | "reverse">( + "idle", + ); + + const startScanning = async () => { + if (scanning || !containerRef.current) return; + + setScanning(true); + setAiProbability(0); + setHighlightedWords([]); + setAnimationPhase("forward"); + + const containerWidth = containerRef.current.offsetWidth - 110; + + // Forward scan + await scannerAnimation.start({ + x: containerWidth, + transition: { duration: scanDuration, ease: "linear" }, + }); + + setAnimationPhase("paused"); + + // Pause + await new Promise((resolve) => setTimeout(resolve, 200)); + + setAnimationPhase("reverse"); + + // Backward scan + await scannerAnimation.start({ + x: "-87%", + transition: { duration: reverseDuration, ease: "linear" }, + }); + + setScanning(false); + setHighlightedWords([]); + setAnimationPhase("idle"); + }; + + useEffect(() => { + let interval: NodeJS.Timeout; + let pauseTimeout: NodeJS.Timeout; + + if (animationPhase === "forward") { + interval = setInterval( + () => { + setAiProbability((prev) => + Math.min(prev + 1, Math.floor(content.length / highlightWords.length)), + ); + }, + (scanDuration * 1000) / 55, + ); + } else if (animationPhase === "paused") { + //delay before starting reverse + pauseTimeout = setTimeout(() => { + setAnimationPhase("reverse"); + }, 200); + } else if (animationPhase === "reverse") { + interval = setInterval( + () => { + setAiProbability((prev) => Math.max(prev - 1, 0)); + }, + (reverseDuration * 1000) / 40, + ); + } + + return () => { + clearInterval(interval); + clearTimeout(pauseTimeout); + }; + }, [animationPhase, scanDuration, reverseDuration, content.length, highlightWords.length]); + + useEffect(() => { + if (scanning && scannerRef.current && contentRef.current) { + const updateHighlightedWords = () => { + const scannerRect = scannerRef.current!.getBoundingClientRect(); + const contentRect = contentRef.current!.getBoundingClientRect(); + const scannerRightEdge = scannerRect.right - contentRect.left; + + const newHighlightedWords = highlightWords.filter((phrase) => { + const phraseElements = contentRef.current!.querySelectorAll(`[data-phrase="${phrase}"]`); + return Array.from(phraseElements).some((element) => { + const elementRect = element.getBoundingClientRect(); + const elementRightEdge = elementRect.right - contentRect.left; + return elementRightEdge <= scannerRightEdge; + }); + }); + + setHighlightedWords(newHighlightedWords); + }; + + const animationFrame = requestAnimationFrame(function animate() { + updateHighlightedWords(); + if (scanning) { + requestAnimationFrame(animate); + } + }); + + return () => cancelAnimationFrame(animationFrame); + } + }, [scanning, highlightWords]); + + const highlightText = (text: string) => { + let result = text; + highlightWords.forEach((phrase) => { + const regex = new RegExp(`(${phrase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})`, "gi"); + result = result.replace( + regex, + (match) => + `${match}`, + ); + }); + return result; + }; + + const renderAiProbability = (probability: number) => { + const digits = probability.toString().padStart(2, "0").split("").map(Number); + + const digitVariants = { + initial: { y: 0 }, + animate: { + y: [0, -30, 0], + transition: { + repeat: Infinity, + repeatType: "loop" as const, + duration: 1.5, + ease: "easeInOut", + }, + }, + }; + + return ( + <> +
+
+ {digits.map((digit, index) => ( + + {[digit, (digit + 1) % 10, (digit + 2) % 10].map((n, i) => ( + + {n} + + ))} + + ))} +
+
+ + ); + }; + + return ( +
+
+

Free AI Content Detector

+

Brand new content in seconds. Remove any form of plagiarism

+
+ + +
+ +
+
+
+
+ + + +
+
+ +
+
+
+ {aiProbability > 0 && renderAiProbability(Math.floor(aiProbability))} + % + AI Content Probability +
+
+
+ + +
+ ); +}; + +export default ContentScanner; diff --git a/animata/icon/hover-interaction.stories.tsx b/animata/icon/hover-interaction.stories.tsx new file mode 100644 index 00000000..9e7bbae2 --- /dev/null +++ b/animata/icon/hover-interaction.stories.tsx @@ -0,0 +1,22 @@ +import HoverInteraction from "@/animata/icon/hover-interaction"; +import { Meta, StoryObj } from "@storybook/react"; + +const meta = { + title: "Icon/Hover Interaction", + component: HoverInteraction, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], + argTypes: {}, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + args: { + title: "instagram", + size: "4", + }, +}; diff --git a/animata/icon/hover-interaction.tsx b/animata/icon/hover-interaction.tsx new file mode 100644 index 00000000..8439489c --- /dev/null +++ b/animata/icon/hover-interaction.tsx @@ -0,0 +1,133 @@ +"use client"; + +import React, { ElementType, useState } from "react"; +import { AnimatePresence, motion } from "framer-motion"; + +// default imports +import { + FigmaLogoIcon, + FramerLogoIcon, + GitHubLogoIcon, + InstagramLogoIcon, + LinkedInLogoIcon, + SquareIcon, + TwitterLogoIcon, +} from "@radix-ui/react-icons"; + +type IconSize = "1" | "2" | "3" | "4"; // source: https://www.radix-ui.com/themes/docs/components/icon-button + +interface IconHoverProps { + title: string; // default is Square + size?: IconSize; // default is 4 +} + +const sizeClasses: Record = { + "1": "w-6 h-6", + "2": "w-7 h-7", + "3": "w-8 h-8", + "4": "w-10 h-10", +}; + +const textSizeClasses: Record = { + "1": "text-sm", + "2": "text-base", + "3": "text-lg", + "4": "text-xl", +}; + +const getIconForTitle = (title: string) => { + const lowercaseTitle = title.toLowerCase().trim(); + const iconMap: { [key: string]: ElementType } = { + framer: FramerLogoIcon, + "twitter/x": TwitterLogoIcon, + instagram: InstagramLogoIcon, + linkedin: LinkedInLogoIcon, + github: GitHubLogoIcon, + figma: FigmaLogoIcon + }; + + // SquareIcon as default + return (iconMap[lowercaseTitle] as ElementType) || SquareIcon; +}; + +// twitter -> Twitter, Twitter -> Twitter, twitter/x -> Twitter/X, Twitter/x -> Twitter/X +const capitalizeWithSlash = (str: string) => { + return str + .split("/") + .map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()) + .join("/"); +}; + +export default function HoverInteraction({ + // default values + title = "Square", + size = "4", +}: IconHoverProps) { + const [isHovered, setIsHovered] = useState(false); + const DynamicIcon = getIconForTitle(title); + + const sizeClass = sizeClasses[size]; + const textSizeClass = textSizeClasses[size]; + + const formattedTitle = capitalizeWithSlash(title); + + const logoVariants = { + hidden: { + opacity: 0, + y: 0, + scale: 0.5, + rotate: 100, + }, + visible: { + opacity: 1, + y: 13, + scale: 1, + rotate: 0, + transition: { + type: "spring", + stiffness: 100, + damping: 15, + duration: 0.3, + }, + }, + exit: { + opacity: 0, + y: 13, + scale: 0.5, + rotate: 100, + transition: { duration: 0.3 }, + }, + }; + + return ( + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + + {formattedTitle} + + + {isHovered && ( + + + + )} + + + ); +} diff --git a/animata/image/images-reveal.stories.tsx b/animata/image/images-reveal.stories.tsx new file mode 100644 index 00000000..8ac7c65a --- /dev/null +++ b/animata/image/images-reveal.stories.tsx @@ -0,0 +1,19 @@ +import ImagesReveal from "@/animata/image/images-reveal"; +import { Meta, StoryObj } from "@storybook/react"; + +const meta = { + title: "Image/Images Reveal", + component: ImagesReveal, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], + argTypes: {}, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + args: {}, +}; diff --git a/animata/image/images-reveal.tsx b/animata/image/images-reveal.tsx new file mode 100644 index 00000000..b414e4c5 --- /dev/null +++ b/animata/image/images-reveal.tsx @@ -0,0 +1,74 @@ +import React from "react"; +import { motion } from "framer-motion"; + +const cards = [ + { + src: "https://images.unsplash.com/photo-1727717768632-f4241a128f50?q=80&w=2889&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", + angle: "8deg", + }, + { + src: "https://images.unsplash.com/photo-1727400068319-565c56633dc3?q=80&w=1911&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", + angle: "-15deg", + }, + { + src: "https://images.unsplash.com/photo-1726551195764-f98a8e8a57c3?q=80&w=2787&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", + angle: "-5deg", + }, + { + src: "https://images.unsplash.com/photo-1727775805114-a87c6bcaf9db?q=80&w=2940&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", + angle: "10deg", + }, + { + src: "https://images.unsplash.com/photo-1614680108604-c23b65f7e7dc?q=80&w=2787&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", + angle: "-5deg", + }, +]; + +interface CustomProps { + index: number; + angle: string; +} + +const cardVariants = { + hidden: { opacity: 0, scale: 0.2 }, + visible: (custom: CustomProps) => ({ + opacity: 1, + scale: 1, + rotate: custom.angle, + transition: { + delay: custom.index * 0.1, + duration: 0.3, + type: "spring", + stiffness: 150, + damping: 20, + mass: 0.5, + }, + }), +}; + +export default function ImagesReveal() { + return ( +
+

Airbnb Image Reveal

+
+ {cards.map((card, i) => ( + + ))} +
+
+ ); +} diff --git a/animata/list/flower-menu.stories.tsx b/animata/list/flower-menu.stories.tsx new file mode 100644 index 00000000..ace2b1fc --- /dev/null +++ b/animata/list/flower-menu.stories.tsx @@ -0,0 +1,45 @@ +import { + Codepen, + Facebook, + Github, + Instagram, + Linkedin, + Twitch, + Twitter, + Youtube, +} from "lucide-react"; + +import FlowerMenu from "@/animata/list/flower-menu"; +import { Meta, StoryObj } from "@storybook/react"; + +const meta = { + title: "List/Flower Menu", + component: FlowerMenu, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], + argTypes: {}, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + args: { + menuItems: [ + { icon: Github, href: "https://github.com/" }, + { icon: Twitter, href: "https://twitter.com/" }, + { icon: Instagram, href: "https://instagram.com/" }, + { icon: Linkedin, href: "https://www.linkedin.com/" }, + { icon: Youtube, href: "https://www.youtube.com/" }, + { icon: Twitch, href: "https://www.twitch.tv/" }, + { icon: Facebook, href: "https://www.facebook.com/" }, + { icon: Codepen, href: "https://www.codepen.io/" }, + ], + iconColor: "#ffffff", + backgroundColor: "rgba(0, 0, 0)", + animationDuration: 700, + togglerSize: 40, + }, +}; diff --git a/animata/list/flower-menu.tsx b/animata/list/flower-menu.tsx new file mode 100644 index 00000000..865150d4 --- /dev/null +++ b/animata/list/flower-menu.tsx @@ -0,0 +1,187 @@ +import { useState } from "react"; +import Link from "next/link"; + +type MenuItem = { + icon: React.ComponentType>; + href: string; +}; + +type FlowerMenuProps = { + menuItems: MenuItem[]; + iconColor?: string; + backgroundColor?: string; + animationDuration?: number; + togglerSize?: number; +}; + +const MenuToggler = ({ + isOpen, + onChange, + backgroundColor, + iconColor, + animationDuration, + togglerSize, + iconSize, +}: { + isOpen: boolean; + onChange: () => void; + backgroundColor: string; + iconColor: string; + animationDuration: number; + togglerSize: number; + iconSize: number; +}) => { + const lineHeight = iconSize * 0.1; + const lineWidth = iconSize * 0.8; + const lineSpacing = iconSize * 0.25; + + return ( + <> + + + + ); +}; + +const MenuItem = ({ + item, + index, + isOpen, + iconColor, + backgroundColor, + animationDuration, + itemCount, + itemSize, + iconSize, +}: { + item: MenuItem; + index: number; + isOpen: boolean; + iconColor: string; + backgroundColor: string; + animationDuration: number; + itemCount: number; + itemSize: number; + iconSize: number; +}) => { + const Icon = item.icon; + return ( +
  • + + + +
  • + ); +}; + +export default function FlowerMenu({ + menuItems, + iconColor = "white", + backgroundColor = "rgba(255, 255, 255, 0.2)", + animationDuration = 500, + togglerSize = 40, +}: FlowerMenuProps) { + const [isOpen, setIsOpen] = useState(false); + const itemCount = menuItems.length; + const itemSize = togglerSize * 2; + const iconSize = Math.max(24, Math.floor(togglerSize * 0.6)); + + return ( + + ); +} diff --git a/animata/list/orbiting-items-3-d.stories.tsx b/animata/list/orbiting-items-3-d.stories.tsx new file mode 100644 index 00000000..d58c3afd --- /dev/null +++ b/animata/list/orbiting-items-3-d.stories.tsx @@ -0,0 +1,26 @@ +import OrbitingItems3D from "@/animata/list/orbiting-items-3-d"; +import { LucideIcons } from "@/animata/list/orbiting-items-3-d"; +import { Meta, StoryObj } from "@storybook/react"; + +const meta = { + title: "List/Orbiting Items 3 D", + component: OrbitingItems3D, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], + argTypes: {}, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + args: { + radiusX: 120, + radiusY: 30, + items: LucideIcons, + duration: 25, + tiltAngle: 360 - 30, + }, +}; diff --git a/animata/list/orbiting-items-3-d.tsx b/animata/list/orbiting-items-3-d.tsx new file mode 100644 index 00000000..6e086c87 --- /dev/null +++ b/animata/list/orbiting-items-3-d.tsx @@ -0,0 +1,172 @@ +import { useEffect, useState } from "react"; +import { Apple, BadgeCent, BadgeInfo, BadgeX, Banana, Bolt } from "lucide-react"; + +import { Icons } from "@/components/icons"; +import { cn } from "@/lib/utils"; + +export const CenterIcon = ( + +); +export const LucideIcons = [ + , + , + , + , + , + , +]; + +interface OrbitingItems3DProps { + /** + * The radius of the ellipse on X-axis in percentage, relative to the container. + */ + radiusX: number; + + /** + * The radius of the ellipse on Y-axis in percentage, relative to the container. + */ + radiusY: number; + + /** + * The angle at which ellipse is tilted to x-axis. + */ + tiltAngle: number; + + /** + * The time taken for the revolution around the center element. + */ + duration: number; + + /** + * The items to orbit around the center of the parent element. + */ + items: React.ReactNode[]; + + /** + * Class name for the background element. + */ + backgroundClassName?: string; + + /** + * Class name for the container element. + */ + containerClassName?: string; + + /** + * Additional classes for the item container. + */ + className?: string; +} + +export default function OrbitingItems3D({ + radiusX = 120, + radiusY = 30, + tiltAngle = 360 - 30, + duration = 25, + items = LucideIcons, + backgroundClassName, + containerClassName, + className, +}: OrbitingItems3DProps) { + // The OrbitingItems3D component creates an animated elliptical orbiting effect for a set of items around a central element. + // It allows for a visually dynamic layout, where items revolve around the center in a smooth, continuous motion, + // creating the illusion of 3D movement. The component provides a range of customizable options to control the orbit, + // including the size of the elliptical path, tilt angle, and animation duration. + + const CalculateItemStyle = ({ + index, + radiusX, + radiusY, + totalItems, + tiltAngle, + duration, + }: { + index: number; + radiusX: number; + radiusY: number; + totalItems: number; + tiltAngle: number; + duration: number; + }) => { + const angleStep = 360 / totalItems; + const [angle, setAngle] = useState(index * angleStep); + useEffect(() => { + const animation = setInterval(() => { + setAngle((prevAngle) => (prevAngle + 1) % 360); + }, duration); + + return () => clearInterval(animation); + }, [duration]); + // Calculate the current angle for the item on the orbit + + const radians = (angle * Math.PI) / 180; + + // X and Y positions before tilt + const x = radiusX * Math.cos(radians); + const y = radiusY * Math.sin(radians); + + // Apply the tilt using rotation matrix + const tiltRadians = (tiltAngle * Math.PI) / 180; + const xTilted = x * Math.cos(tiltRadians) - y * Math.sin(tiltRadians); + const yTilted = x * Math.sin(tiltRadians) + y * Math.cos(tiltRadians); + const zIndex = angle > 180 ? -1 : 1; + const scale = angle < 180 ? 1.2 : 1.0; + + return { + left: `${50 + xTilted}%`, + top: `${50 + yTilted}%`, + transform: `translate(-50%, -50%) scale(${scale})`, + zIndex: zIndex, + transition: "transform 0.8s ease-in-out", + }; + }; + + const reverse = cn("transition-transform ease-linear direction-reverse repeat-infinite"); + + return ( +
    +
    +
    + {CenterIcon} + {items.map((item, index) => { + return ( +
    +
    {item}
    +
    + ); + })} +
    +
    + ); +} diff --git a/animata/list/transaction-list.stories.tsx b/animata/list/transaction-list.stories.tsx new file mode 100644 index 00000000..600c85c8 --- /dev/null +++ b/animata/list/transaction-list.stories.tsx @@ -0,0 +1,86 @@ +import { ChefHat, Receipt, Signal } from "lucide-react"; + +import TransactionList from "@/animata/list/transaction-list"; +import { Meta, StoryObj } from "@storybook/react"; + +const meta = { + title: "List/Transaction List", + component: TransactionList, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], + argTypes: { transactions: { control: { type: "object" } } }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const transactions = [ + { + id: "67593", + name: "Netflix", + type: "Subscription", + amount: -6.99, + date: "September 26", + time: "12:01 am", + icon: , + paymentMethod: "Credit Card", + cardLastFour: "9342", + cardType: "visa", + }, + { + id: "67482", + name: "Verizon", + type: "Mobile Recharge", + amount: -4.05, + date: "September 24", + time: "05:18 pm", + icon: , + paymentMethod: "Credit Card", + cardLastFour: "2316", + cardType: "mastercard", + }, + { + id: "52363", + name: "Figma", + type: "Subscription", + amount: -15.0, + date: "September 15", + time: "01:11 pm", + icon: , + paymentMethod: "Credit Card", + cardLastFour: "9342", + cardType: "visa", + }, + { + id: "54635", + name: "Rive", + type: "Subscription", + amount: -32.0, + date: "September 16", + time: "02:11 pm", + icon: , + paymentMethod: "Credit Card", + cardLastFour: "9342", + cardType: "mastercard", + }, + { + id: "52342", + name: "Big Belly Burger", + type: "Restaurant", + amount: -12.05, + date: "September 12", + time: "09:06 pm", + icon: , + paymentMethod: "Credit Card", + cardLastFour: "2316", + cardType: "visa", + }, +]; + +export const Primary: Story = { + args: { + transactions: transactions, + }, +}; diff --git a/animata/list/transaction-list.tsx b/animata/list/transaction-list.tsx new file mode 100644 index 00000000..9d34f1f4 --- /dev/null +++ b/animata/list/transaction-list.tsx @@ -0,0 +1,199 @@ +import React, { useState } from "react"; +import { AnimatePresence, motion } from "framer-motion"; +import { ArrowRight, CreditCard, X } from "lucide-react"; + +interface Transaction { + id: string; + name: string; + type: string; + amount: number; + date: string; + time: string; + icon: React.ReactNode; + paymentMethod: string; + cardLastFour: string; + cardType?: string; +} + +export default function TransactionList({ transactions }: { transactions: Transaction[] }) { + const [selectedTransaction, setSelectedTransaction] = useState(null); + + return ( +
    + + + {!selectedTransaction ? ( + +

    Transactions

    +
    + {transactions.map((transaction) => ( + setSelectedTransaction(transaction)} + > +
    + + {transaction.icon} + +
    + + {transaction.name} + + + {transaction.type} + +
    +
    + + ${Math.abs(transaction.amount).toFixed(2)} + +
    + ))} +
    + + All Transactions + +
    + ) : ( + +
    + + + {selectedTransaction.icon} + + + +
    +
    +
    + + {selectedTransaction.name} + + + {selectedTransaction.type} + +
    + + ${Math.abs(selectedTransaction.amount).toFixed(2)} + +
    + +
    +

    #{selectedTransaction.id}

    +

    {selectedTransaction.date}

    +

    {selectedTransaction.time}

    +
    +
    +

    Paid Via {selectedTransaction.paymentMethod}

    +
    + +

    XXXX {selectedTransaction.cardLastFour}

    +

    + {selectedTransaction.cardType === "visa" ? : } +

    +
    +
    +
    +
    + )} +
    +
    +
    + ); +} + +const MasterCardLogo = () => { + return ( + + + + + + + ); +}; + +const VisaLogo = () => { + return ( + + + + + + + + + + ); +}; diff --git a/animata/progress/animatedtimeline.stories.tsx b/animata/progress/animatedtimeline.stories.tsx new file mode 100644 index 00000000..fea084af --- /dev/null +++ b/animata/progress/animatedtimeline.stories.tsx @@ -0,0 +1,68 @@ +import Animatedtimeline, { TimelineEvent } from "@/animata/progress/animatedtimeline"; +import { Meta, StoryObj } from "@storybook/react"; + +const meta = { + title: "Progress/Animatedtimeline", + component: Animatedtimeline, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], + argTypes: {}, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const events = [ + { + id: "1", + title: "Event 1", + description: "This is the first event", + date: "2021-01-01", + }, + { + id: "2", + title: "Event 2", + description: "This is the second event", + date: "2021-02-01", + }, + { + id: "3", + title: "Event 3", + description: "This is the third event", + date: "2021-03-01", + }, +]; + +const customEventRender = (event: TimelineEvent) => ( +
    +

    {event.title}

    +

    {event.description}

    + {event.date} +
    +); + +const onEventClick = () => null; + +export const Primary: Story = { + args: { + events: events, + title: "Timeline", + containerClassName: "", + timelineStyles: { + lineColor: "#d1d5db", + activeLineColor: "#22c55e", + dotColor: "#d1d5db", + activeDotColor: "#22c55e", + dotSize: "1.5rem", + titleColor: "inherit", + descriptionColor: "inherit", + dateColor: "inherit", + }, + customEventRender: customEventRender, + onEventHover: () => null, + onEventClick: onEventClick, + initialActiveIndex: -1, + }, +}; diff --git a/animata/progress/animatedtimeline.tsx b/animata/progress/animatedtimeline.tsx new file mode 100644 index 00000000..09c19c42 --- /dev/null +++ b/animata/progress/animatedtimeline.tsx @@ -0,0 +1,215 @@ +"use client"; + +import React, { useState } from "react"; +import { motion } from "framer-motion"; + +import { cn } from "@/lib/utils"; + +export interface TimelineEvent { + id: string; + title: string; + description?: string; + date?: string; + [key: string]: unknown; // Allow additional custom fields +} + +interface TimelineItemProps { + event: TimelineEvent; + isActive: boolean; + isFirst: boolean; + isLast: boolean; + onHover: (index: number | null) => void; + index: number; + activeIndex: number | null; + styles: TimelineStyles; + customRender?: (event: TimelineEvent) => React.ReactNode; +} + +interface TimelineStyles { + lineColor: string; + activeLineColor: string; + dotColor: string; + activeDotColor: string; + dotSize: string; + titleColor: string; + descriptionColor: string; + dateColor: string; +} + +const TimelineItem: React.FC = ({ + event, + isActive, + isLast, + onHover, + index, + activeIndex, + styles, + customRender, +}) => { + const fillDelay = activeIndex !== null ? Math.max(0, (index - 1) * 0.1) : 0; + const fillDuration = activeIndex !== null ? Math.max(0.2, 0.5 - index * 0.1) : 0.5; + + return ( + onHover(index)} + onHoverEnd={() => onHover(null)} + initial={{ opacity: 0, y: 50 }} + animate={{ opacity: 1, y: 0 }} + transition={{ duration: 0.5 }} + > +
    +
    + +
    + +
    +
    + {customRender ? ( + customRender(event) + ) : ( + <> +

    + {event.title} +

    +

    {event.description}

    + + {event.date} + + + )} +
    +
    + ); +}; + +interface AnimatedTimelineProps { + events: TimelineEvent[]; + className?: string; + styles?: Partial; + customEventRender?: (event: TimelineEvent) => React.ReactNode; + onEventHover?: (event: TimelineEvent | null) => void; + onEventClick?: (event: TimelineEvent) => void; + initialActiveIndex?: number; +} + +const defaultStyles: TimelineStyles = { + lineColor: "#d1d5db", + activeLineColor: "#22c55e", + dotColor: "#d1d5db", + activeDotColor: "#22c55e", + dotSize: "1.5rem", + titleColor: "inherit", + descriptionColor: "inherit", + dateColor: "inherit", +}; + +export function AnimatedTimeline({ + events, + className = "", + styles: customStyles = {}, + customEventRender, + onEventHover, + onEventClick, + initialActiveIndex, +}: AnimatedTimelineProps) { + const [activeIndex, setActiveIndex] = useState(initialActiveIndex ?? null); + const styles = { ...defaultStyles, ...customStyles }; + + const handleHover = (index: number | null) => { + setActiveIndex(index); + onEventHover?.(index !== null ? events[index] : null); + }; + + return ( +
    + {events.map((event, index) => ( +
    onEventClick?.(event)}> + +
    + ))} +
    + ); +} + +interface AnimatedTimelinePageProps { + events?: TimelineEvent[]; + title?: string; + containerClassName?: string; + timelineStyles?: Partial; + customEventRender?: (events: TimelineEvent) => React.ReactNode; + onEventHover?: (events: TimelineEvent | null) => void; + onEventClick?: (events: TimelineEvent) => void; + initialActiveIndex?: number; +} + +export default function AnimatedTimelinePage({ + events, + title, + containerClassName, + timelineStyles, + customEventRender, + onEventHover, + onEventClick, + initialActiveIndex, +}: AnimatedTimelinePageProps) { + const DefaultEvents = [ + { id: "1", title: "Event 1", description: "Description 1", date: "2023-01-01" }, + { id: "2", title: "Event 2", description: "Description 2", date: "2023-02-01" }, + { id: "3", title: "Event 3", description: "Description 3", date: "2023-03-01" }, + ]; + const defaultTitle = "Timeline"; + + return ( +
    +

    {title || defaultTitle}

    + +
    + ); +} diff --git a/animata/section/pricing.stories.tsx b/animata/section/pricing.stories.tsx new file mode 100644 index 00000000..284ef767 --- /dev/null +++ b/animata/section/pricing.stories.tsx @@ -0,0 +1,56 @@ +import { Meta, StoryObj } from "@storybook/react"; + +import Pricing from "./pricing"; + +const meta = { + title: "Section/Pricing", + component: Pricing, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], + argTypes: { + width: { + control: { type: "select" }, + options: ["sm", "md", "lg", "xl"], + }, + outerRadius: { + control: { type: "select" }, + options: ["normal", "rounded", "moreRounded"], + }, + padding: { + control: { type: "select" }, + options: ["small", "medium", "large"], + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + args: { + plans: [ + { name: "Free", monthlyPrice: "$0.00", yearlyPrice: "$0.00" }, + { name: "Starter", monthlyPrice: "$9.99", yearlyPrice: "$99.99", popular: true }, + { name: "Pro", monthlyPrice: "$19.99", yearlyPrice: "$199.99" }, + ], + width: "md", + outerRadius: "rounded", + padding: "medium", + }, +}; + +export const CustomPlans: Story = { + args: { + plans: [ + { name: "Basic", monthlyPrice: "$4.99", yearlyPrice: "$49.99" }, + { name: "Standard", monthlyPrice: "$14.99", yearlyPrice: "$149.99", popular: true }, + { name: "Premium", monthlyPrice: "$24.99", yearlyPrice: "$249.99" }, + { name: "Enterprise", monthlyPrice: "$49.99", yearlyPrice: "$499.99" }, + ], + width: "xl", + outerRadius: "moreRounded", + padding: "large", + }, +}; diff --git a/animata/section/pricing.tsx b/animata/section/pricing.tsx new file mode 100644 index 00000000..692157a4 --- /dev/null +++ b/animata/section/pricing.tsx @@ -0,0 +1,223 @@ +import React, { useEffect, useRef, useState } from "react"; +import { AnimatePresence, motion } from "framer-motion"; + +import { cn } from "@/lib/utils"; + +interface Plan { + name: string; + monthlyPrice: string; + yearlyPrice: string; + popular?: boolean; +} + +interface PricingProps { + plans: Plan[]; + onPlanSelect?: (plan: string) => void; + onCycleChange?: (cycle: "Monthly" | "Yearly") => void; + width?: "sm" | "md" | "lg" | "xl"; + outerRadius?: "normal" | "rounded" | "moreRounded"; + padding?: "small" | "medium" | "large"; +} + +const widthClasses = { + sm: "w-full sm:w-[300px]", + md: "w-full sm:w-[300px] md:w-[500px]", + lg: "w-full sm:w-[300px] md:w-[500px] lg:w-[768px]", + xl: "w-full sm:w-[300px] md:w-[500px] lg:w-[768px] xl:w-[1024px]", +}; + +const outerRadiusClasses = { + normal: "rounded-[16px]", + rounded: "rounded-[24px]", + moreRounded: "rounded-[32px]", +}; + +const paddingClasses = { + small: "p-2", + medium: "p-3", + large: "p-4", +}; + +const innerRadiusClasses = { + normal: "rounded-xl", + rounded: "rounded-2xl", + moreRounded: "rounded-3xl", +}; + +export default function Pricing({ + plans, + width = "lg", + outerRadius = "rounded", + padding = "medium", +}: PricingProps) { + const [selectedPlan, setSelectedPlan] = useState("Basic"); + const [billingCycle, setBillingCycle] = useState<"Monthly" | "Yearly">("Monthly"); + + const handlePlanSelect = (planName: string) => { + setSelectedPlan(planName); + }; + + const handleCycleChange = (cycle: "Monthly" | "Yearly") => { + setBillingCycle(cycle); + }; + + return ( +
    +
    +
    + +
    + {["Monthly", "Yearly"].map((cycle) => ( + { + e.stopPropagation(); + handleCycleChange(cycle as "Monthly" | "Yearly"); + }} + whileTap={{ scale: 0.95 }} + > + {cycle} + + ))} +
    +
    +
    + + {plans.map((plan) => ( + handlePlanSelect(plan.name)} + whileHover={{ scale: 1.02 }} + whileTap={{ scale: 0.98 }} + layout + > + + {selectedPlan === plan.name && ( + + )} + +
    +
    + {plan.name} + {plan.popular && ( + Popular + )} +
    + + {selectedPlan === plan.name && ( + + )} + +
    +
    + +
    +
    + ))} + + + Get Started + +
    + ); +} +interface AnimatedPriceProps { + monthlyPrice: string; + yearlyPrice: string; + billingCycle: "Monthly" | "Yearly"; +} + +function AnimatedPrice({ + monthlyPrice, + yearlyPrice, + billingCycle, +}: AnimatedPriceProps): React.JSX.Element { + const [price, setPrice] = useState(monthlyPrice); + const animationRef = useRef(null); + + useEffect(() => { + const targetPrice = billingCycle === "Monthly" ? monthlyPrice : yearlyPrice; + const startValue = parseFloat(price.replace(/[^0-9.-]+/g, "")); + const endValue = parseFloat(targetPrice.replace(/[^0-9.-]+/g, "")); + const duration = 50; // Animation duration in milliseconds + const startTime = Date.now(); + + const animatePrice = () => { + const elapsedTime = Date.now() - startTime; + const progress = Math.min(elapsedTime / duration, 1); + const currentValue = startValue + (endValue - startValue) * progress; + + setPrice(`$${currentValue.toFixed(2)}`); + + if (progress < 1) { + animationRef.current = requestAnimationFrame(animatePrice); + } else { + setPrice(targetPrice); + } + }; + + animatePrice(); + + return () => { + if (animationRef.current) { + cancelAnimationFrame(animationRef.current); + } + }; + }, [price, billingCycle, monthlyPrice, yearlyPrice]); + + return ( +
    + {price} + /{billingCycle.toLowerCase().slice(0, -2)} +
    + ); +} diff --git a/animata/widget/fund-widget.stories.tsx b/animata/widget/fund-widget.stories.tsx new file mode 100644 index 00000000..78785783 --- /dev/null +++ b/animata/widget/fund-widget.stories.tsx @@ -0,0 +1,25 @@ +import FundWidget from "@/animata/widget/fund-widget"; +import { Meta, StoryObj } from "@storybook/react"; + +const meta = { + title: "Widget/Fund Widget", + component: FundWidget, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], + argTypes: {}, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + args: { + funds: [ + { value: "2.7Cr", change: 12, label: "Stocks" }, + { value: "3.5Cr", change: -8, label: "Funds" }, + { value: "1.2Cr", change: 6, label: "Deposits" }, + ], + }, +}; diff --git a/animata/widget/fund-widget.tsx b/animata/widget/fund-widget.tsx new file mode 100644 index 00000000..f64280ad --- /dev/null +++ b/animata/widget/fund-widget.tsx @@ -0,0 +1,180 @@ +import { useEffect, useState } from "react"; +import { AnimatePresence, motion, PanInfo } from "framer-motion"; + +import { cn } from "@/lib/utils"; + +type Fund = { + value: string; + change: number; + label: string; +}; + +interface FundWidgetProps { + /** + * The array which contains all the funds with their value, changes, and label. + */ + funds: Fund[]; + + /** + * Class name for the background element. + */ + backgroundClassName?: string; + + /** + * Class name for the container element. + */ + containerClassName?: string; +} + +export default function FundWidget({ + funds = [ + { value: "2.7Cr", change: 12, label: "Stocks" }, + { value: "3.5Cr", change: -8, label: "Funds" }, + { value: "1.2Cr", change: 6, label: "Deposits" }, + ], + backgroundClassName, + containerClassName, +}: FundWidgetProps) { + const len = funds.length; + + const [[activeDiv, direction], setDirection] = useState([0, 0]); + const [dragDistance, setDragDistance] = useState(0); + + // Reset dragDistance after the drag ends to remove blur/rotate effects + useEffect(() => { + if (dragDistance !== 0) { + const timer = setTimeout(() => { + setDragDistance(0); + }, 500); + return () => clearTimeout(timer); + } + }, [activeDiv, dragDistance]); + + const sliderVariants = { + incoming: (direction: number) => ({ + y: direction > 0 ? "100%" : "-100%", + scale: 1.0, + opacity: 0, + }), + active: { y: 0, scale: 1, opacity: 1 }, + exit: (direction: number) => ({ + y: direction > 0 ? "100%" : "-100%", + scale: 1, + opacity: 0.2, + }), + }; + + const sliderTransition = { + duration: 0.5, + ease: [0.56, 0.03, 0.12, 1.04], + }; + + const swipeToAction = (direction: number) => { + const newDiv = activeDiv + direction; + if (newDiv < 0 || newDiv >= len) return; + + setDirection([newDiv, direction]); + }; + + const draghandler = (dragInfo: PanInfo) => { + const dragDistanceY = dragInfo.offset.y; + const swipeThreshold = 20; + + // Only swipe down if not at the first div (activeDiv !== 0) + if (dragDistanceY > swipeThreshold) { + swipeToAction(-1); + } + // Only swipe up if not at the last div (activeDiv !== len - 1) + else if (dragDistanceY < -swipeThreshold) { + swipeToAction(1); + } + + setDragDistance(0); + }; + + const skipToDiv = (divId: number) => { + let changeDirection = 1; + if (divId > activeDiv) { + changeDirection = 1; + } else if (divId < activeDiv) { + changeDirection = -1; + } + setDirection([divId, changeDirection]); + }; + + const blurValue = Math.min(Math.abs(dragDistance / 20), 10); + const rotateYValue = Math.min(dragDistance / 10, 15); + + return ( + <> +
    +
    + +
    +
    + draghandler(dragInfo)} + onDrag={(event, info) => setDragDistance(info.offset.y)} + style={{ + filter: `blur(${blurValue}px)`, + transform: `rotateY(${rotateYValue}deg)`, + }} + > +
    +

    {funds[activeDiv].value}

    + {funds[activeDiv].change < 0 ? ( +

    + {funds[activeDiv].change}% ↓ +

    + ) : ( +

    + {funds[activeDiv].change}% ↑ +

    + )} +

    + {funds[activeDiv].label} +

    +
    +
    +
    + {funds.map((_, index) => { + return ( + skipToDiv(index)} + > + ); + })} +
    +
    +
    +
    +
    +
    + + ); +} diff --git a/animata/widget/music-stack-interaction.stories.tsx b/animata/widget/music-stack-interaction.stories.tsx new file mode 100644 index 00000000..39319b3c --- /dev/null +++ b/animata/widget/music-stack-interaction.stories.tsx @@ -0,0 +1,53 @@ +import MusicStackInteraction from "@/animata/widget/music-stack-interaction"; +import { Meta, StoryObj } from "@storybook/react"; + +const meta = { + title: "Widget/Music Stack Interaction", + component: MusicStackInteraction, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], + argTypes: {}, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + args: { + albums: [ + { + id: 1, + title: "The Dark Side of the Moon", + artist: "Pink Floyd", + cover: "https://images.unsplash.com/photo-1569424758782-cba94e6165fd", + }, + { + id: 2, + title: "Abbey Road", + artist: "The Beatles", + cover: "https://images.unsplash.com/photo-1516410529446-2c777cb7366d", + }, + { + id: 3, + title: "Thriller", + artist: "Michael Jackson", + cover: "https://images.unsplash.com/photo-1559406041-c7d2b2e98690", + }, + { + id: 4, + title: "The Wall", + artist: "Pink Floyd", + cover: "https://images.unsplash.com/photo-1528822234686-beae35cab346", + }, + ], + }, + render: (args) => { + return ( +
    + +
    + ); + }, +}; diff --git a/animata/widget/music-stack-interaction.tsx b/animata/widget/music-stack-interaction.tsx new file mode 100644 index 00000000..f32c6ab9 --- /dev/null +++ b/animata/widget/music-stack-interaction.tsx @@ -0,0 +1,126 @@ +import React, { useState } from "react"; +import { motion } from "framer-motion"; +import { Layers, LayoutGrid } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +const carouselStyles = { + perspective: "1000px", + overflow: "hidden", +}; + +const carouselInnerStyles: React.CSSProperties = { + display: "flex", + transformStyle: "preserve-3d", + transition: "transform 2s", + justifyContent: "center", + alignItems: "center", + height: "100%", +}; + +const carouselItemStyles: React.CSSProperties = { + minWidth: "200px", + marginLeft: "-180px", + transform: "rotateY(15deg) translateZ(300px) translateX(-50px)", + backfaceVisibility: "hidden", + boxShadow: "0 4px 8px rgba(0, 0, 0, 0.2)", + transition: "transform 2s, box-shadow 2s", +}; + +const carouselItemFirstChildStyles: React.CSSProperties = { + minWidth: "200px", + marginLeft: "20px", + transform: "rotateY(5deg) translateZ(300px) translateX(0)", + backfaceVisibility: "hidden", + boxShadow: "0 4px 8px rgba(0, 0, 0, 0.2)", + transition: "transform 2s, box-shadow 2s", +}; + +interface albumsProps { + /* + * Array of album objects + */ + albums: { + id: number; + title: string; + artist: string; + cover: string; + }[]; +} + +export default function MusicStackInteraction({ albums }: albumsProps) { + const [isGridView, setIsGridView] = useState(true); + + const handleToggleView = () => { + setIsGridView(!isGridView); + }; + + return ( +
    + + + {albums.map((album, index) => ( +
    + + + + {album.title} + + + {album.artist} + + +
    + ))} +
    +
    + + +
    +
    + +
    +
    + +
    +
    +
    +
    + ); +} diff --git a/animata/widget/team-clock.stories.tsx b/animata/widget/team-clock.stories.tsx new file mode 100644 index 00000000..1fe6feb4 --- /dev/null +++ b/animata/widget/team-clock.stories.tsx @@ -0,0 +1,47 @@ +import TeamClock from "@/animata/widget/team-clock"; +import { Meta, StoryObj } from "@storybook/react"; + +const testTeamClockProps = { + users: [ + { + name: "User 1", + city: "New York", + country: "USA", + timeDifference: "-5", + pfp: "https://avatar.vercel.sh/1", + }, + { + name: "User 2", + city: "London", + country: "UK", + timeDifference: "+5", + pfp: "https://avatar.vercel.sh/2", + }, + ], + clockSize: 250, + animationDuration: 0.3, + accentColor: "#34D399", + backgroundColor: "#ECFDF5", + textColor: "#065F46", + borderColor: "#D1FAE5", + hoverBackgroundColor: "#D1FAE5", + showSeconds: true, + use24HourFormat: false, +}; + +const meta = { + title: "Widget/Team Clock", + component: TeamClock, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], + argTypes: {}, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + args: testTeamClockProps, +}; diff --git a/animata/widget/team-clock.tsx b/animata/widget/team-clock.tsx new file mode 100644 index 00000000..15d4e448 --- /dev/null +++ b/animata/widget/team-clock.tsx @@ -0,0 +1,469 @@ +"use client"; + +import { useEffect, useMemo, useRef, useState } from "react"; +import { AnimatePresence, motion } from "framer-motion"; + +import { cn } from "@/lib/utils"; + +interface TeamClockProps { + users: Array<{ + name: string; + city: string; + country: string; + timeDifference: string; + pfp: string; + }>; + clockSize: number; + animationDuration: number; + accentColor: string; + backgroundColor: string; + textColor: string; + borderColor: string; + hoverBackgroundColor: string; + showSeconds: boolean; + use24HourFormat: boolean; +} + +export default function TeamClock({ + users, + clockSize, + animationDuration, + accentColor = "#000", + backgroundColor = "#ffffff", + textColor = "#1f2937", + borderColor = "#e5e7eb", + hoverBackgroundColor = "#f3f4f6", + showSeconds = false, + use24HourFormat = false, +}: TeamClockProps) { + const [isExpanded, setIsExpanded] = useState(false); + const [angle, setAngle] = useState(0); + const [currentTime, setCurrentTime] = useState(new Date()); + const [isMobile, setIsMobile] = useState(false); + const [selectedUser, setSelectedUser] = useState(null); + const [hoveredUser, setHoveredUser] = useState(null); + + useEffect(() => { + const checkMobile = () => { + setIsMobile(window.innerWidth < 768); + }; + + checkMobile(); + window.addEventListener("resize", checkMobile); + + return () => window.removeEventListener("resize", checkMobile); + }, []); + + useEffect(() => { + const timer = setInterval(() => { + setCurrentTime(new Date()); + }, 1000); + + return () => clearInterval(timer); + }, []); + + const handleToggle = () => { + setIsExpanded(!isExpanded); + }; + + const handleUserSelect = (userName: string, timeDifference: string) => { + if (selectedUser === userName) { + setSelectedUser(null); + setAngle(0); + } else { + setSelectedUser(userName); + setAngle(parseInt(timeDifference) * 30); + } + }; + + const handleUserHover = (userName: string | null, timeDifference: string | null) => { + if (userName && timeDifference) { + setHoveredUser(userName); + setAngle(parseInt(timeDifference) * 30); + } else { + setHoveredUser(null); + if (!selectedUser) { + setAngle(0); + } else { + const selectedUserData = users.find((user) => user.name === selectedUser); + if (selectedUserData) { + setAngle(parseInt(selectedUserData.timeDifference) * 30); + } + } + } + }; + + return ( + <> + +
    +
    +

    Team

    + {!isMobile && ( + + )} +
    +
    + +
    +
    + {currentTime.toLocaleTimeString([], { + hour: use24HourFormat ? "2-digit" : "numeric", + minute: "2-digit", + second: showSeconds ? "2-digit" : undefined, + hour12: !use24HourFormat, + })} +
    +
    + + {/* Add vertical dividing line */} + {isExpanded && !isMobile && ( +
    + )} + + + {(isExpanded || isMobile) && ( + + + {users.map((user, index) => ( + + ))} + + + )} + +
    + + ); +} + +interface ClockProps { + angle: number; + pressed: boolean; + size: number; + animationDuration: number; + accentColor: string; + textColor: string; + backgroundColor: string; +} + +function Clock({ + angle, + size, + animationDuration, + accentColor, + textColor, + backgroundColor, +}: ClockProps) { + const [time, setTime] = useState(null); + const gradientRef = useRef(null); + + useEffect(() => { + setTime(new Date()); + const interval = setInterval(() => { + setTime(new Date()); + }, 1000); + + return () => clearInterval(interval); + }, []); + + useEffect(() => { + const isClockwise = angle > 0; + if (gradientRef.current && time) { + const hours = time.getHours(); + const minutes = time.getMinutes(); + const hourDegrees = (hours % 12) * 30 + minutes * 0.5; + gradientRef.current.style.background = isClockwise + ? `conic-gradient(from ${hourDegrees}deg, rgba(0,200,0,0.5), rgba(0,200,0,0) ${angle}deg)` + : `conic-gradient(from ${ + hourDegrees + angle + }deg, rgba(200,0,0,0.3), rgba(200,0,0,0.0) ${-angle}deg)`; + } + }, [angle, time]); + + if (!time) { + return null; + } + + const hours = time.getHours(); + const minutes = time.getMinutes(); + const seconds = time.getSeconds(); + const hourDegrees = (hours % 12) * 30 + minutes * 0.5; + const minuteDegrees = minutes * 6; + const secondDegrees = seconds * 6; + + return ( +
    +
    + {Array.from({ length: 12 }, (_, i) => ( +
    +
    +
    + ))} + + + + +
    +
    +
    + ); +} + +interface ListElementProp { + name: string; + city: string; + country: string; + timeDifference: string; + pfp: string; + onSelect: (name: string, timeDifference: string) => void; + onHover: (name: string | null, timeDifference: string | null) => void; + isSelected: boolean; + isHovered: boolean; + currentTime: Date; + animationDuration: number; + accentColor: string; + textColor: string; + hoverBackgroundColor: string; +} + +function ListElement(props: ListElementProp) { + const [isHovered, setIsHovered] = useState(false); + const [isMobile, setIsMobile] = useState(false); + + useEffect(() => { + const checkMobile = () => { + setIsMobile(window.innerWidth < 768); + }; + + checkMobile(); + window.addEventListener("resize", checkMobile); + + return () => window.removeEventListener("resize", checkMobile); + }, []); + + const handleEnter = () => { + if (!isMobile) { + setIsHovered(true); + props.onHover(props.name, props.timeDifference); + } + }; + + const handleLeave = () => { + if (!isMobile) { + setIsHovered(false); + props.onHover(null, null); + } + }; + + const handleClick = () => { + props.onSelect(props.name, props.timeDifference); + }; + + const localTime = useMemo(() => { + const hourDifference = parseInt(props.timeDifference); + const newTime = new Date(props.currentTime); + newTime.setHours(newTime.getHours() + hourDifference); + return newTime.toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + }); + }, [props.currentTime, props.timeDifference]); + + return ( + + {props.name} +
    +
    + {props.name} +
    + + {!props.isSelected && !isHovered && ( + + {localTime} + + )} + {(props.isSelected || isHovered) && ( + + {parseInt(props.timeDifference) === 0 + ? "+ 0 Hours" + : `${props.timeDifference} Hours`} + + )} + +
    +
    +
    {`${props.city}, ${props.country}`}
    +
    +
    + ); +} + +type ToggleButtonProps = { + onClick: (isToggled: boolean) => void; + accentColor: string; + textColor: string; +}; + +function ToggleButton({ onClick, accentColor, textColor, ...props }: ToggleButtonProps) { + const [isToggled, setIsToggled] = useState(false); + + const handleClick = () => { + setIsToggled(!isToggled); + onClick(!isToggled); + }; + + return ( + + + {isToggled ? "Hide List" : "Show List"} + +
    + + +
    +
    + ); +} diff --git a/app/_landing/hero.tsx b/app/_landing/hero.tsx index 1004b5b4..f9ced922 100644 --- a/app/_landing/hero.tsx +++ b/app/_landing/hero.tsx @@ -1,6 +1,5 @@ import Link from "next/link"; -import { Announcement } from "@/components/announcement"; import { Icons } from "@/components/icons"; import { PageHeaderDescription } from "@/components/page-header"; import { buttonVariants } from "@/components/ui/button"; @@ -38,9 +37,6 @@ export default function Hero() { radial-gradient(at 66% 84%, hsla(89,66%,79%,1) 0px, transparent 50%)`, }} /> - - -
    diff --git a/config/docs.ts b/config/docs.ts index c24432fa..034ca70b 100644 --- a/config/docs.ts +++ b/config/docs.ts @@ -105,10 +105,18 @@ const sidebarNav: SidebarNavItem[] = [ title: "Container", items: createLinks("container"), }, + { + title: "Accordion", + items: createLinks("accordion"), + }, { title: "Card", items: createLinks("card"), }, + { + title: "Section", + items: createLinks("section"), + }, { title: "Icon", items: createLinks("icon"), @@ -168,6 +176,14 @@ const sidebarNav: SidebarNavItem[] = [ href: "/docs/skeleton", items: createLinks("skeleton"), }, + { + title: "Feature cards", + items: createLinks("feature-cards"), + }, + { + title: "Floating Action Buttons", + items: createLinks("fabs"), + }, ] .filter((category) => Boolean(category.items?.length || category.label)) .sort((a, b) => { diff --git a/content/blog/hacktoberfest-2024.mdx b/content/blog/hacktoberfest-2024.mdx index 2db873a7..bb0737c6 100644 --- a/content/blog/hacktoberfest-2024.mdx +++ b/content/blog/hacktoberfest-2024.mdx @@ -43,7 +43,7 @@ Join our [Discord community](https://discord.gg/YfvqMf5MTE) to connect with othe ### Contribution Rewards -We’re offering a **$50 reward** to one lucky contributor based on a raffle system. +We’re offering a **$100 reward** to one lucky contributor based on a raffle system. ### Contribution Rules @@ -70,7 +70,7 @@ The winner will be selected randomly from all entries. All contributions must be submitted by **October 31, 11:59 PM UTC**. 6. **Payment Eligibility:** - To qualify for the **$50 reward**, you must be able to **receive international payments** via platforms like PayPal. + To qualify for the **$100 reward**, you must be able to **receive international payments** via platforms like PayPal. 7. **Others:** - The reward will be paid within 30 days of the contest end date. diff --git a/content/docs/accordion/faq.mdx b/content/docs/accordion/faq.mdx new file mode 100644 index 00000000..59051503 --- /dev/null +++ b/content/docs/accordion/faq.mdx @@ -0,0 +1,38 @@ +--- +title: Faq +description: its an faq accordion that looks like an chating interface with smooth animations +author: anshu_code +--- + + + +## Installation + + +Install dependencies + +```bash +npm install framer-motion lucide-react +``` + +Run the following command + +It will create a new file `faq.tsx` inside the `components/animata/accordion` directory. + +```bash +mkdir -p components/animata/accordion && touch components/animata/accordion/faq.tsx +``` + +Paste the code{" "} + +Open the newly created file and paste the following code: + +```jsx file=/animata/accordion/faq.tsx + +``` + + + +## Credits + +Built by [Anshuman](https://github.com/anshuman008) diff --git a/content/docs/button/animated-follow-button.mdx b/content/docs/button/animated-follow-button.mdx new file mode 100644 index 00000000..063df174 --- /dev/null +++ b/content/docs/button/animated-follow-button.mdx @@ -0,0 +1,38 @@ +--- +title: Animated Follow Button +description: The Animated Follow Button is an interactive UI component with customizable entrance animations and dynamic text changes on click, offering flexible styling options. Perfect for engaging user interactions like follow or subscribe actions. +author: R0X4R +--- + + + +## Installation + + +Install dependencies + +```bash +npm install framer-motion +``` + +Run the following command + +It will create a new file `animated-follow-button.tsx` inside the `components/animata/button` directory. + +```bash +mkdir -p components/animata/button && touch components/animata/button/animated-follow-button.tsx +``` + +Paste the code + +Open the newly created file and paste the following code: + +```jsx file=/animata/button/animated-follow-button.tsx + +``` + + + +## Credits + + Built by [Eshan Singh](https://github.com/R0X4R) with [Framer Motion](https://www.framer.com/motion/). diff --git a/content/docs/button/index.mdx b/content/docs/button/index.mdx index e52fbc2c..472a78e4 100644 --- a/content/docs/button/index.mdx +++ b/content/docs/button/index.mdx @@ -13,6 +13,7 @@ import ExternalLinkButton from "@/animata/button/external-link-button"; import AlgoliaButtonWhite from "@/animata/button/algolia-white-button"; import AlgoliaButtonBlue from "@/animata/button/algolia-blue-button"; import Duolingo from "@/animata/button/duolingo"; +import SlideArrowButton from "@/animata/button/slide-arrow-button"; @@ -100,4 +101,12 @@ import Duolingo from "@/animata/button/duolingo"; ```` + + + + ```tsx file=/animata/button/slide-arrow-button.tsx copyId="slide-arrow-button" + + ```` + + diff --git a/content/docs/button/ripple-button.mdx b/content/docs/button/ripple-button.mdx new file mode 100644 index 00000000..0d4e44f6 --- /dev/null +++ b/content/docs/button/ripple-button.mdx @@ -0,0 +1,33 @@ +--- +title: Ripple Button +description: Button with ripple effect on mouse position. +labels: ["requires interaction", "hover"] +author: Abhi_Hertz +--- + + + +## Installation + + +Run the following command + +It will create a new file called `ripple-button.tsx` inside the `compoents/animata/button` directory. + +```bash +mkdir -p components/animata/button && touch components/animata/button/ripple-button.tsx +``` + +Paste the code + +Open the newly create file and paste the following code: + +```jsx file=/animata/button/ripple-button.tsx + +``` + + + +## Credits + + Built by [Abhinandan](hhttps://github.com/AE-Hertz/) diff --git a/content/docs/button/slide-arrow-button.mdx b/content/docs/button/slide-arrow-button.mdx new file mode 100644 index 00000000..391ec2c8 --- /dev/null +++ b/content/docs/button/slide-arrow-button.mdx @@ -0,0 +1,39 @@ +--- +title: Slide Arrow Button +description: An arrow button which slides on hover +author: wolfofdalalst +labels: ["requires interaction", "hover"] +--- + + + +## Installation + + +Install dependencies + +```bash +npm install lucide-react +``` + +Run the following command + +It will create a new file `slide-arrow-button.tsx` inside the `components/animata/button` directory. + +```bash +mkdir -p components/animata/button && touch components/animata/button/slide-arrow-button.tsx +``` + +Paste the code{" "} + +Open the newly created file and paste the following code: + +```jsx file=/animata/button/slide-arrow-button.tsx + +``` + + + +## Credits + +Built by [Ayush Gupta](https://github.com/wolfofdalalst) diff --git a/content/docs/card/case-study-card.mdx b/content/docs/card/case-study-card.mdx new file mode 100644 index 00000000..c707f4c6 --- /dev/null +++ b/content/docs/card/case-study-card.mdx @@ -0,0 +1,33 @@ +--- +title: Case Study Card +description: An interactive case study card that expands on hover, revealing a hidden tab with a clickable link to read comments. +author: parankarj +--- + + + +## Installation + + + +Run the following command + +It will create a new file `case-study-card.tsx` inside the `components/animata/card` directory. + +```bash +mkdir -p components/animata/card && touch components/animata/card/case-study-card.tsx +``` + +Paste the code{" "} + +Open the newly created file and paste the following code: + +```jsx file=/animata/card/case-study-card.tsx + +``` + + + +## Credits + +Built by [Adriana Fruchter](https://github.com/webdevNYC) diff --git a/content/docs/card/comment-reply-card.mdx b/content/docs/card/comment-reply-card.mdx new file mode 100644 index 00000000..6807aa07 --- /dev/null +++ b/content/docs/card/comment-reply-card.mdx @@ -0,0 +1,38 @@ +--- +title: Comment Reply Card +description: This new React component allows users to submit comments through an input field. Once a comment is submitted, it dynamically appears at the top of the comment list. +author: m_jinprince +--- + + + +## Installation + + +Install dependencies + +```bash +npm install framer-motion lucide-react +``` + +Run the following command + +It will create a new file `comment-reply-card.tsx` inside the `components/animata/card` directory. + +```bash +mkdir -p components/animata/card && touch components/animata/card/comment-reply-card.tsx +``` + +Paste the code{" "} + +Open the newly created file and paste the following code: + +```jsx file=/animata/card/comment-reply-card.tsx + +``` + + + +## Credits + +Built by [Prince Yadav](https://github.com/prince981620) diff --git a/content/docs/card/fluid-tabs.mdx b/content/docs/card/fluid-tabs.mdx new file mode 100644 index 00000000..df593bb5 --- /dev/null +++ b/content/docs/card/fluid-tabs.mdx @@ -0,0 +1,52 @@ +--- +title: Fluid Tabs +description: The component is a sliding animation card +author: RudraSankha +--- + + + +## Installation + + +Install dependencies + +```bash +npm install framer-motion lucide-react +``` + +Update `tailwind.config.js` + +Add the following to your tailwind.config.js file. + +```json +module.exports = { + theme: { + extend: { + } + } +} +``` + +Run the following command + +It will create a new file `fluid-tabs.tsx` inside the `components/animata/card` directory. + +```bash +mkdir -p components/animata/card && touch components/animata/card/fluid-tabs.tsx +``` + +Paste the code{" "} + +Open the newly created file and paste the following code: + +```jsx file=/animata/card/fluid-tabs.tsx + +``` + + + +## Credits + +Built by [Rudra Sankha Sinhamahapatra](https://github.com/Rudra-Sankha-Sinhamahapatra) +Twitter Handle [Rudra Sankha](https://x.com/RudraSankha) diff --git a/content/docs/card/integration-pills.mdx b/content/docs/card/integration-pills.mdx new file mode 100644 index 00000000..8001859c --- /dev/null +++ b/content/docs/card/integration-pills.mdx @@ -0,0 +1,33 @@ +--- +title: Integration pills +description: A component that displays a list of services it can integrate with. +author: shoomankhatri +--- + + + +## Installation + + + +Run the following command + +It will create a new file `integration-pills.tsx` inside the `components/animata/card` directory. + +```bash +mkdir -p components/animata/card && touch components/animata/card/integration-pills.tsx +``` + +Paste the code{" "} + +Open the newly created file and paste the following code: + +```jsx file=/animata/card/integration-pills.tsx + +``` + + + +## Credits + +Built by [Suman Khatri](https://github.com/shoomankhatri) diff --git a/content/docs/card/notice-card.mdx b/content/docs/card/notice-card.mdx new file mode 100644 index 00000000..b6607ad1 --- /dev/null +++ b/content/docs/card/notice-card.mdx @@ -0,0 +1,38 @@ +--- +title: Notice Card +description: A card component for displaying important notices with an accept toggle. +labels: ["requires interaction", "toggle"] +author: AE-Hertz +--- + + + +## Installation + + +Install dependencies + +```bash +npm install framer-motion +``` + +Run the following command + +It will create a new file `notice-card.tsx` inside the `components/animata/card` directory. + +```bash +mkdir -p components/animata/card && touch components/animata/card/notice-card.tsx +``` + +Paste the code + +Open the newly created file and paste the following code: + +```tsx file=/animata/card/notice-card.tsx +``` + + + +## Credits + +Built by [Abhinandan](https://github.com/AE-Hertz) diff --git a/content/docs/card/notification-card.mdx b/content/docs/card/notification-card.mdx new file mode 100644 index 00000000..f333d1ea --- /dev/null +++ b/content/docs/card/notification-card.mdx @@ -0,0 +1,38 @@ +--- +title: Notification Card +description: Simple notification like componenet that beautiully expands to show its contents. +author: m_jinprince +--- + + + +## Installation + + +Install dependencies + +```bash +npm install framer-motion +``` + +Run the following command + +It will create a new file `notification-card.tsx` inside the `components/animata/card` directory. + +```bash +mkdir -p components/animata/card && touch components/animata/card/notification-card.tsx +``` + +Paste the code{" "} + +Open the newly created file and paste the following code: + +```jsx file=/animata/card/notification-card.tsx + +``` + + + +## Credits + +Built by [Prince Yadav](https://github.com/prince981620) diff --git a/content/docs/card/notify-user-info.mdx b/content/docs/card/notify-user-info.mdx new file mode 100644 index 00000000..a893727c --- /dev/null +++ b/content/docs/card/notify-user-info.mdx @@ -0,0 +1,38 @@ +--- +title: Notify User Info +description: This component is a notification animation based on user information. +author: MEbandhan +--- + + + +## Installation + + +Install dependencies + +```bash +npm install framer-motion +``` + +Run the following command + +It will create a new file `notify-user-info.tsx` inside the `components/animata/card` directory. + +```bash +mkdir -p components/animata/card && touch components/animata/card/notify-user-info.tsx +``` + +Paste the code{" "} + +Open the newly created file and paste the following code: + +```jsx file=/animata/card/notify-user-info.tsx + +``` + + + +## Credits + +Built by [Bandhan Majumder](https://github.com/bandhan-majumder) diff --git a/content/docs/card/reminder-scheduler.mdx b/content/docs/card/reminder-scheduler.mdx new file mode 100644 index 00000000..f8bdb8e6 --- /dev/null +++ b/content/docs/card/reminder-scheduler.mdx @@ -0,0 +1,54 @@ +--- +title: Reminder Scheduler +description: a simple card to set up recurring reminders along with frequency and repetition. +author: m_jinprince +labels: ["requires interaction", "toggle switch"] +--- + + + +## Installation + + + +Update `tailwind.config.js` + +Add the following to your tailwind.config.js file. + +```js +theme: { + extend: { + colors: { + foreground: "hsl(var(--foreground))", + }, + transitionTimingFunction: { + slow: "cubic-bezier(.405, 0, .025, 1)", + "minor-spring": "cubic-bezier(0.18,0.89,0.82,1.04)", + } + }, + }, +``` + +Run the following command + +It will create a new file `reminder-scheduler.tsx` inside the `components/animata/card` directory. + +```bash +mkdir -p components/animata/card && touch components/animata/card/reminder-scheduler.tsx +``` + +Paste the code{" "} + +Open the newly created file and paste the following code: + +```jsx file=/animata/card/reminder-scheduler.tsx + +``` + + + +## Credits + +Built by [Prince Yadav](https://github.com/prince981620) + +[hari](https://github.com/hari) diff --git a/content/docs/card/score-card.mdx b/content/docs/card/score-card.mdx new file mode 100644 index 00000000..10b29ad1 --- /dev/null +++ b/content/docs/card/score-card.mdx @@ -0,0 +1,39 @@ +--- +title: Score Card +description: This Score-card component is designed to track and update scores for two teams in real-time with a simple animation showing which team scored. +author: m_jinprince +labels: ["requires interaction", "Click score button"] +--- + + + +## Installation + + +Install dependencies + +```bash +npm install framer-motion +``` + +Run the following command + +It will create a new file called `score-card.tsx` inside the `components/animata/card` directory. + +```bash +mkdir -p components/animata/card && touch components/animata/card/score-card.tsx +``` + +Paste the code + +Open the newly created file and paste the following code: + +```jsx file=/animata/card/score-card.tsx + +``` + + + +## Credits + +Built by [Prince Yadav](https://github.com/prince981620) diff --git a/content/docs/card/subscribe-card.mdx b/content/docs/card/subscribe-card.mdx new file mode 100644 index 00000000..575414c4 --- /dev/null +++ b/content/docs/card/subscribe-card.mdx @@ -0,0 +1,38 @@ +--- +title: Subscribe Card +description: When the cursor hovers over the card then the card skews and when hovers over the button the check mark appears. +author: PrithwiHegde +--- + + + +## Installation + + +Install dependencies + +```bash +npm install lucide-react +``` + +Run the following command + +It will create a new file `subscribe-card.tsx` inside the `components/animata/card` directory. + +```bash +mkdir -p components/animata/card && touch components/animata/card/subscribe-card.tsx +``` + +Paste the code{" "} + +Open the newly created file and paste the following code: + +```jsx file=/animata/card/subscribe-card.tsx + +``` + + + +## Credits + +Built by [Prithwi Hegde](https://github.com/Prithwi32) diff --git a/content/docs/card/survey-card.mdx b/content/docs/card/survey-card.mdx new file mode 100644 index 00000000..a5e6aa02 --- /dev/null +++ b/content/docs/card/survey-card.mdx @@ -0,0 +1,33 @@ +--- +title: Survey Card +description: showing result of survey on hover over card +labels: ["requires interaction", "hover"] +author: Mahlawat2001 +--- + + + +## Installation + + +Run the following command + +It will create a new file `survey-card.tsx` inside the `components/animata/card` directory. + +```bash +mkdir -p components/animata/card && touch components/animata/card/survey-card.tsx +``` + +Paste the code{" "} + +Open the newly created file and paste the following code: + +```jsx file=/animata/card/survey-card.tsx + +``` + + + +## Credits + +Built by [Mohit Ahlawat](https://github.com/mohitahlawat2001) diff --git a/content/docs/card/webhooks-card.mdx b/content/docs/card/webhooks-card.mdx new file mode 100644 index 00000000..a3e724f6 --- /dev/null +++ b/content/docs/card/webhooks-card.mdx @@ -0,0 +1,34 @@ +--- +title: Web Hooks +description: A card component with animated boxes, lines, and a ball. The component changes style on hover. +labels: ["requires interaction", "hover"] +author: Pavan kumar +--- + + + +## Installation + + + +Run the following command + +It will create a new file `WebHooks-card.tsx` inside the `components/animata/card` directory. + +```bash +mkdir -p components/animata/card && touch components/animata/card/WebHooks-card.tsx +``` + +Paste the code{" "} + +Open the newly created file and paste the following code: + +```jsx file=/animata/card/WebHooks-card.tsx + +``` + + + +## Credits + +Built by [Pavan kumar](https://github.com/pavankumar07s) diff --git a/content/docs/container/animated-dock.mdx b/content/docs/container/animated-dock.mdx new file mode 100644 index 00000000..a88eff89 --- /dev/null +++ b/content/docs/container/animated-dock.mdx @@ -0,0 +1,38 @@ +--- +title: Animated Dock +description: A sleek dock-style navigation bar, inspired by macOS, that combines glassmorphic design with functionality. With smooth animations and responsive icons, it enhances navigation for a modern web application. +author: R0X4R +--- + + + +## Installation + + +Install dependencies + +```bash +npm install framer-motion lucide-react +``` + +Run the following command + +It will create a new file `animated-dock.tsx` inside the `components/animata/container` directory. + +```bash +mkdir -p components/animata/container && touch components/animata/container/animated-dock.tsx +``` + +Paste the code + +Open the newly created file and paste the following code: + +```jsx file=/animata/container/animated-dock.tsx + +``` + + + +## Credits + + Built by [Eshan Singh](https://github.com/R0X4R) with the help of [Framer Motion](https://www.framer.com/motion/). Inspired by [Build UI](https://buildui.com/recipes/magnified-dock) diff --git a/content/docs/fabs/speed-dial.mdx b/content/docs/fabs/speed-dial.mdx new file mode 100644 index 00000000..fc1c3be9 --- /dev/null +++ b/content/docs/fabs/speed-dial.mdx @@ -0,0 +1,38 @@ +--- +title: Speed Dial +description: Speed Dial +author: masabinhok +--- + + + +## Installation + + +Install dependencies + +```bash +npm install lucide-react +``` + +Run the following command + +It will create a new file `speed-dial.tsx` inside the `components/animata/fabs` directory. + +```bash +mkdir -p components/animata/fabs && touch components/animata/fabs/speed-dial.tsx +``` + +Paste the code{" "} + +Open the newly created file and paste the following code: + +```jsx file=/animata/fabs/speed-dial.tsx + +``` + + + +## Credits + +Built by [Sabin Shrestha](https://github.com/masabinhok) diff --git a/content/docs/feature-cards/confirmation-message.mdx b/content/docs/feature-cards/confirmation-message.mdx new file mode 100644 index 00000000..b8a18247 --- /dev/null +++ b/content/docs/feature-cards/confirmation-message.mdx @@ -0,0 +1,38 @@ +--- +title: Confirmation Message +description: An animated component which shows success message with custom name and description. +author: i_v1shal_ +--- + + + +## Installation + + +Install dependencies + +```bash +npm install framer-motion +``` + +Run the following command + +It will create a new file `confirmation-message.tsx` inside the `components/animata/feature-cards` directory. + +```bash +mkdir -p components/animata/feature-cards && touch components/animata/feature-cards/confirmation-message.tsx +``` + +Paste the code{" "} + +Open the newly created file and paste the following code: + +```jsx file=/animata/feature-cards/confirmation-message.tsx + +``` + + + +## Credits + +Built by [Vishal](https://github.com/Pikachu-345) diff --git a/content/docs/feature-cards/content-scan.mdx b/content/docs/feature-cards/content-scan.mdx new file mode 100644 index 00000000..550c2406 --- /dev/null +++ b/content/docs/feature-cards/content-scan.mdx @@ -0,0 +1,54 @@ +--- +title: Content Scan +description: A scanning component to highlight detected words to predict AI content probability +author: MEbandhan +--- + + + +## Installation + + +Install dependencies + +```bash +npm install framer-motion +``` + +Update `tailwind.config.js` + +Add the following to your tailwind.config.js file. + +```json +module.exports = { + theme: { + extend: { + backgroundImage: { + "custom-gradient": "linear-gradient(to left, rgba(136,127,242,0.7) 0%, transparent 100%)", + }, + }, + } +} +``` + +Run the following command + +It will create a new file `content-scan.tsx` inside the `components/animata/feature-cards` directory. + +```bash +mkdir -p components/animata/feature-cards && touch components/animata/feature-cards/content-scan.tsx +``` + +Paste the code{" "} + +Open the newly created file and paste the following code: + +```jsx file=/animata/feature-cards/content-scan.tsx + +``` + + + +## Credits + +Built by [Bandhan Majumder](https://github.com/bandhan-majumder) diff --git a/content/docs/icon/hover-interaction.mdx b/content/docs/icon/hover-interaction.mdx new file mode 100644 index 00000000..c966abd9 --- /dev/null +++ b/content/docs/icon/hover-interaction.mdx @@ -0,0 +1,49 @@ +--- +title: Hover Interaction +description: This is a component which shows icon based on text hovered by user. By default it has alreay imported Twitter/x, Framer, Figma, Instagram, GitHub, LinkedIn, and it shows Square-Box icon by default it user tries to get other icons without importing. Importing steps are provided below. +author: MEbandhan +--- + + + +## Installation + + +Install dependencies + +```bash +npm install framer-motion @radix-ui/react-icons +``` + +Run the following command + +It will create a new file `hover-interaction.tsx` inside the `components/animata/icon` directory. + +```bash +mkdir -p components/animata/icon && touch components/animata/icon/hover-interaction.tsx +``` + +Paste the code + +Open the newly created file and paste the following code: + +```jsx file=/animata/icon/hover-interaction.tsx + +``` + + + +Use the component with default/non-default interacitons + +defaults are mentioned in the description + +**Other than defaults** + +1. Go to the [radix-ui icon page](https://www.radix-ui.com/icons) +2. Search for the icon. +3. Import the icon in the copied code. If the icon name is Twitter Logo, the import will be TwitterLogoIcon +4. Add switch case for that logo in **lower case**. + +## Credits + +Built by [Bandhan Majumder](https://github.com/bandhan-majumder) diff --git a/content/docs/image/images-reveal.mdx b/content/docs/image/images-reveal.mdx new file mode 100644 index 00000000..b89f9801 --- /dev/null +++ b/content/docs/image/images-reveal.mdx @@ -0,0 +1,39 @@ +--- +title: Images Reveal +description: An image reveal animation +author: mansidhamne +labels: ["requires interaction", "hover"] +--- + + + +## Installation + + +Install dependencies + +```bash +npm install framer-motion +``` + +Run the following command + +It will create a new file `images-reveal.tsx` inside the `components/animata/image` directory. + +```bash +mkdir -p components/animata/image && touch components/animata/image/images-reveal.tsx +``` + +Paste the code{" "} + +Open the newly created file and paste the following code: + +```jsx file=/animata/image/images-reveal.tsx + +``` + + + +## Credits + +Built by [mansidhamne](https://github.com/mansidhamne) diff --git a/content/docs/list/flower-menu.mdx b/content/docs/list/flower-menu.mdx new file mode 100644 index 00000000..c2077a5a --- /dev/null +++ b/content/docs/list/flower-menu.mdx @@ -0,0 +1,33 @@ +--- +title: Flower Menu +description: A circular flower menu with several icons and a central close button. +author: arjuncodess +--- + + + +## Installation + + + +Run the following command + +It will create a new file `flower-menu.tsx` inside the `components/animata/list` directory. + +```bash +mkdir -p components/animata/list && touch components/animata/list/flower-menu.tsx +``` + +Paste the code + +Open the newly created file and paste the following code: + +```jsx file=/animata/list/flower-menu.tsx + +``` + + + +## Credits + +Built by [Arjun Vijay Prakash](https://github.com/arjuncodess). diff --git a/content/docs/list/orbiting-items-3-d.mdx b/content/docs/list/orbiting-items-3-d.mdx new file mode 100644 index 00000000..f894503c --- /dev/null +++ b/content/docs/list/orbiting-items-3-d.mdx @@ -0,0 +1,60 @@ +--- +title: Orbiting Items 3D +description: List component with orbiting items. The items orbit around the center of an element in 3D Ellipse. +author: Pikachu-345 +--- + + + +## Installation + + +Install dependencies + +```bash +npm install lucide-react +``` + +Update `tailwind.config.js` + +Add the following to your tailwind.config.js file. + +```json +module.exports = { + theme: { + extend: { + keyframes: { + float: { + '0%, 100%': { transform: 'translateY(0)' }, + '50%': { transform: 'translateY(-40px)' }, + }, + }, + animation: { + float: 'float 3s ease-in-out infinite', + }, + } + } +} +``` + +Run the following command + +It will create a new file `orbiting-items-3-d.tsx` inside the `components/animata/list` directory. + +```bash +mkdir -p components/animata/list && touch components/animata/list/orbiting-items-3-d.tsx +``` + +Paste the code{" "} + +Open the newly created file and paste the following code: + +```jsx file=/animata/list/orbiting-items-3-d.tsx + +``` + + + +## Credits + +Built by [Vishal](https://github.com/Pikachu-345) diff --git a/content/docs/list/transaction-list.mdx b/content/docs/list/transaction-list.mdx new file mode 100644 index 00000000..b2550b85 --- /dev/null +++ b/content/docs/list/transaction-list.mdx @@ -0,0 +1,39 @@ +--- +title: Transaction List +description: A simple component to list all the recent transaction. +author: m_jinprince +labels: ["requires interaction", "Click any recent Transaction"] +--- + + + +## Installation + + +Install dependencies + +```bash +npm install framer-motion lucide-react +``` + +Run the following command + +It will create a new file `transaction-list.tsx` inside the `components/animata/list` directory. + +```bash +mkdir -p components/animata/list && touch components/animata/list/transaction-list.tsx +``` + +Paste the code{" "} + +Open the newly created file and paste the following code: + +```jsx file=/animata/list/transaction-list.tsx + +``` + + + +## Credits + +Built by [Prince Yadav](https://github.com/prince981620) diff --git a/content/docs/progress/animatedtimeline.mdx b/content/docs/progress/animatedtimeline.mdx new file mode 100644 index 00000000..aa9a9930 --- /dev/null +++ b/content/docs/progress/animatedtimeline.mdx @@ -0,0 +1,38 @@ +--- +title: Animated Timeline +description: The Animated Timeline component is an interactive, visually appealing timeline that responds to user interaction. Built with Framer Motion and React, this component highlights key events or milestones in a vertical timeline structure.When a user hovers over a specific timeline item, the associated circular dot and all previous dots in the sequence turn green, indicating progression. The dot size also enlarges slightly, enhancing the focus on the current event. The component offers a sleek and smooth animation experience, perfect for showcasing chronological steps, milestones, or achievements in an engaging and user-friendly manner.This component is highly customizable, allowing easy modifications to the timeline content and styling, making it suitable for diverse applications such as resumes, project timelines, or product roadmaps. +author: VishalKumar03__ +--- + + + +## Installation + + +Install dependencies + +```bash +npm install framer-motion lucide-react +``` + +Run the following command + +It will create a new file `animatedtimeline.tsx` inside the `components/animata/progress` directory. + +```bash +mkdir -p components/animata/progress && touch components/animata/progress/animatedtimeline.tsx +``` + +Paste the code + +Open the newly created file and paste the following code: + +```jsx file=/animata/progress/animatedtimeline.tsx + +``` + + + +## Credits + +Built by [Vishal Kumar](https://github.com/vishal-kumar3) diff --git a/content/docs/section/pricing.mdx b/content/docs/section/pricing.mdx new file mode 100644 index 00000000..20aa0532 --- /dev/null +++ b/content/docs/section/pricing.mdx @@ -0,0 +1,39 @@ +--- +title: Pricing +description: Pricing component that display the pricing options of various plans in an sleek and interactive way +author: SatyamVyas04 +--- + + + +## Installation + + +Install dependencies + +```bash +npm install framer-motion lucide-react +``` + +Run the following command + +It will create a new file `pricing.tsx` inside the `components/animata/section` directory. + +```bash +mkdir -p components/animata/section && touch components/animata/section/pricing.tsx +``` + +Paste the code{" "} + +Open the newly created file and paste the following code: + +```jsx file=/animata/section/pricing.tsx + +``` + + + +## Credits + +Built by [SatyamVyas04](https://github.com/SatyamVyas04) +Inspired by [Nitish Khagwal](https://x.com/nitishkmrk) diff --git a/content/docs/widget/fund-widget.mdx b/content/docs/widget/fund-widget.mdx new file mode 100644 index 00000000..1eedc78c --- /dev/null +++ b/content/docs/widget/fund-widget.mdx @@ -0,0 +1,38 @@ +--- +title: Fund Widget +description: This component is a financial dashboard-like UI element for showing the current status of the funds. +author: i_v1shal_ +--- + + + +## Installation + + +Install dependencies + +```bash +npm install framer-motion +``` + +Run the following command + +It will create a new file `fund-widget.tsx` inside the `components/animata/widget` directory. + +```bash +mkdir -p components/animata/widget && touch components/animata/widget/fund-widget.tsx +``` + +Paste the code{" "} + +Open the newly created file and paste the following code: + +```jsx file=/animata/widget/fund-widget.tsx + +``` + + + +## Credits + +Built by [Vishal](https://github.com/Pikachu-345) diff --git a/content/docs/widget/music-stack-interaction.mdx b/content/docs/widget/music-stack-interaction.mdx new file mode 100644 index 00000000..0f406e84 --- /dev/null +++ b/content/docs/widget/music-stack-interaction.mdx @@ -0,0 +1,38 @@ +--- +title: Music Stack Interaction +description: widget for Music stack and unstacking +author: Mahlawat2001 +--- + + + +## Installation + + +Install dependencies + +```bash +npm install lucide-react framer-motion +``` + +Run the following command + +It will create a new file `music-stack-interaction.tsx` inside the `components/animata/widget` directory. + +```bash +mkdir -p components/animata/widget && touch components/animata/widget/music-stack-interaction.tsx +``` + +Paste the code{" "} + +Open the newly created file and paste the following code: + +```jsx file=/animata/widget/music-stack-interaction.tsx + +``` + + + +## Credits + +Built by [Mohit Ahlawat](https://github.com/mohitahlawat2001) diff --git a/content/docs/widget/team-clock.mdx b/content/docs/widget/team-clock.mdx new file mode 100644 index 00000000..c463c802 --- /dev/null +++ b/content/docs/widget/team-clock.mdx @@ -0,0 +1,38 @@ +--- +title: Team Clock +description: A customizable, animated clock displaying multiple time zones for remote teams. +author: arjuncodess +--- + + + +## Installation + + +Install dependencies + +```bash +npm install framer-motion +``` + +Run the following command + +It will create a new file `team-clock.tsx` inside the `components/animata/widget` directory. + +```bash +mkdir -p components/animata/widget && touch components/animata/widget/team-clock.tsx +``` + +Paste the code{" "} + +Open the newly created file and paste the following code: + +```jsx file=/animata/widget/team-clock.tsx + +``` + + + +## Credits + +Built by [Arjun Vijay Prakash](https://github.com/arjuncodess). diff --git a/tailwind.config.ts b/tailwind.config.ts index 3c51c61f..930d8ab5 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -15,7 +15,8 @@ const config = { extend: { backgroundImage: { striped: - "repeating-linear-gradient(45deg, #3B3A3D, #3B3A3D 5px, transparent 5px, transparent 20px)", + "repeating-linear-gradient(45deg, #3B3A3D 0px, #3B3A3D 5px, transparent 5px, transparent 20px)", + "custom-gradient": "linear-gradient(to left, rgba(136,127,242,0.7) 0%, transparent 100%)", }, colors: { border: "hsl(var(--border))", @@ -198,6 +199,10 @@ const config = { "50%": { opacity: "1" }, "100%": { opacity: "0" }, }, + float: { + "0%, 100%": { transform: "translateY(0)" }, + "50%": { transform: "translateY(-40px)" }, + }, }, animation: { fill: "fill 1s forwards", @@ -214,6 +219,7 @@ const config = { meteor: "meteor var(--duration) var(--delay) ease-in-out infinite", trail: "trail var(--duration) linear infinite", led: "led 100ms ease-in-out", + float: "float 3s ease-in-out infinite", }, transitionTimingFunction: { slow: "cubic-bezier(.405, 0, .025, 1)",