From 4b8d7a25bff53d68038ec45805ff842ace2bdbba Mon Sep 17 00:00:00 2001 From: nayanrdeveloper Date: Wed, 4 Feb 2026 11:42:19 +0530 Subject: [PATCH] feat: add animated core icon components, documentation, Storybook stories, and a new CLI tool. --- app/docs/icons/available-icons.tsx | 129 +++++++++++ app/docs/icons/bell-icon-basic.tsx | 25 +++ app/docs/icons/gallery-wrapper.tsx | 2 +- app/docs/icons/global-icon-basic.tsx | 25 +++ app/docs/icons/global-search-icon-basic.tsx | 25 +++ app/docs/icons/heart-icon-basic.tsx | 25 +++ app/docs/icons/like-icon-basic.tsx | 26 +++ app/docs/icons/mail-stack-icon-basic.tsx | 25 +++ app/docs/icons/microphone-icon-basic.tsx | 25 +++ app/docs/icons/paper-plane-icon-basic.tsx | 25 +++ app/docs/icons/phone-icon-basic.tsx | 1 - app/docs/icons/share-icon-basic.tsx | 25 +++ app/docs/icons/tools-icon-basic.tsx | 26 +++ app/docs/icons/trash-icon-basic.tsx | 25 +++ app/docs/icons/trophy-icon-basic.tsx | 25 +++ app/docs/icons/user-icon-basic.tsx | 26 +++ cli/package-lock.json | 4 +- cli/package.json | 2 +- components/core/bell-icon.tsx | 146 +++++++++++++ components/core/building-icon.tsx | 2 +- components/core/global-icon.tsx | 139 ++++++++++++ components/core/global-search-icon.tsx | 129 +++++++++++ components/core/heart-icon.tsx | 109 ++++++++++ components/core/like-icon.tsx | 122 +++++++++++ components/core/mail-stack-icon.tsx | 147 +++++++++++++ components/core/microphone-icon.tsx | 128 +++++++++++ components/core/network-icon.tsx | 1 - components/core/paper-plane-icon.tsx | 136 ++++++++++++ components/core/share-icon.tsx | 151 +++++++++++++ components/core/tools-icon.tsx | 230 ++++++++++++++++++++ components/core/trash-icon.tsx | 144 ++++++++++++ components/core/trophy-icon.tsx | 126 +++++++++++ components/core/user-icon.tsx | 119 ++++++++++ lib/shiki.ts | 19 +- public/e/bell-icon-basic.json | 18 ++ public/e/building-icon-basic.json | 2 +- public/e/global-icon-basic.json | 18 ++ public/e/global-search-icon-basic.json | 18 ++ public/e/heart-icon-basic.json | 18 ++ public/e/like-icon-basic.json | 18 ++ public/e/mail-stack-icon-basic.json | 18 ++ public/e/microphone-icon-basic.json | 18 ++ public/e/network-icon-basic.json | 2 +- public/e/paper-plane-icon-basic.json | 18 ++ public/e/phone-icon-basic.json | 2 +- public/e/share-icon-basic.json | 18 ++ public/e/tools-icon-basic.json | 18 ++ public/e/trash-icon-basic.json | 18 ++ public/e/trophy-icon-basic.json | 18 ++ public/e/user-icon-basic.json | 18 ++ scripts/registry-examples.ts | 172 +++++++++++++++ stories/bell-icon.stories.tsx | 51 +++++ stories/global-icon.stories.tsx | 60 +++++ stories/global-search-icon.stories.tsx | 51 +++++ stories/heart-icon.stories.tsx | 51 +++++ stories/like-icon.stories.tsx | 51 +++++ stories/mail-stack-icon.stories.tsx | 51 +++++ stories/microphone-icon.stories.tsx | 51 +++++ stories/paper-plane-icon.stories.tsx | 51 +++++ stories/share-icon.stories.tsx | 51 +++++ stories/tools-icon.stories.tsx | 54 +++++ stories/trash-icon.stories.tsx | 51 +++++ stories/trophy-icon.stories.tsx | 51 +++++ stories/user-icon.stories.tsx | 51 +++++ 64 files changed, 3387 insertions(+), 14 deletions(-) create mode 100644 app/docs/icons/bell-icon-basic.tsx create mode 100644 app/docs/icons/global-icon-basic.tsx create mode 100644 app/docs/icons/global-search-icon-basic.tsx create mode 100644 app/docs/icons/heart-icon-basic.tsx create mode 100644 app/docs/icons/like-icon-basic.tsx create mode 100644 app/docs/icons/mail-stack-icon-basic.tsx create mode 100644 app/docs/icons/microphone-icon-basic.tsx create mode 100644 app/docs/icons/paper-plane-icon-basic.tsx create mode 100644 app/docs/icons/share-icon-basic.tsx create mode 100644 app/docs/icons/tools-icon-basic.tsx create mode 100644 app/docs/icons/trash-icon-basic.tsx create mode 100644 app/docs/icons/trophy-icon-basic.tsx create mode 100644 app/docs/icons/user-icon-basic.tsx create mode 100644 components/core/bell-icon.tsx create mode 100644 components/core/global-icon.tsx create mode 100644 components/core/global-search-icon.tsx create mode 100644 components/core/heart-icon.tsx create mode 100644 components/core/like-icon.tsx create mode 100644 components/core/mail-stack-icon.tsx create mode 100644 components/core/microphone-icon.tsx create mode 100644 components/core/paper-plane-icon.tsx create mode 100644 components/core/share-icon.tsx create mode 100644 components/core/tools-icon.tsx create mode 100644 components/core/trash-icon.tsx create mode 100644 components/core/trophy-icon.tsx create mode 100644 components/core/user-icon.tsx create mode 100644 public/e/bell-icon-basic.json create mode 100644 public/e/global-icon-basic.json create mode 100644 public/e/global-search-icon-basic.json create mode 100644 public/e/heart-icon-basic.json create mode 100644 public/e/like-icon-basic.json create mode 100644 public/e/mail-stack-icon-basic.json create mode 100644 public/e/microphone-icon-basic.json create mode 100644 public/e/paper-plane-icon-basic.json create mode 100644 public/e/share-icon-basic.json create mode 100644 public/e/tools-icon-basic.json create mode 100644 public/e/trash-icon-basic.json create mode 100644 public/e/trophy-icon-basic.json create mode 100644 public/e/user-icon-basic.json create mode 100644 stories/bell-icon.stories.tsx create mode 100644 stories/global-icon.stories.tsx create mode 100644 stories/global-search-icon.stories.tsx create mode 100644 stories/heart-icon.stories.tsx create mode 100644 stories/like-icon.stories.tsx create mode 100644 stories/mail-stack-icon.stories.tsx create mode 100644 stories/microphone-icon.stories.tsx create mode 100644 stories/paper-plane-icon.stories.tsx create mode 100644 stories/share-icon.stories.tsx create mode 100644 stories/tools-icon.stories.tsx create mode 100644 stories/trash-icon.stories.tsx create mode 100644 stories/trophy-icon.stories.tsx create mode 100644 stories/user-icon.stories.tsx diff --git a/app/docs/icons/available-icons.tsx b/app/docs/icons/available-icons.tsx index ba6ed50..e37c243 100644 --- a/app/docs/icons/available-icons.tsx +++ b/app/docs/icons/available-icons.tsx @@ -14,6 +14,19 @@ import { CalendarIcon } from '@/components/core/calendar-icon'; import { MobileStoreIcon } from '@/components/core/mobile-store-icon'; import { BalanceIcon } from '@/components/core/balance-icon'; import { BuildingIcon } from '@/components/core/building-icon'; +import { GlobalIcon } from '@/components/core/global-icon'; +import { UserIcon } from '@/components/core/user-icon'; +import { GlobalSearchIcon } from '@/components/core/global-search-icon'; +import { ToolsIcon } from '@/components/core/tools-icon'; +import { TrophyIcon } from '@/components/core/trophy-icon'; +import { MicrophoneIcon } from '@/components/core/microphone-icon'; +import { LikeIcon } from '@/components/core/like-icon'; +import { BellIcon } from '@/components/core/bell-icon'; +import { HeartIcon } from '@/components/core/heart-icon'; +import { TrashIcon } from '@/components/core/trash-icon'; +import { ShareIcon } from '@/components/core/share-icon'; +import { PaperPlaneIcon } from '@/components/core/paper-plane-icon'; +import { MailStackIcon } from '@/components/core/mail-stack-icon'; import PhoneIconBasic from './phone-icon-basic'; import RocketIconBasic from './rocket-icon-basic'; @@ -30,6 +43,19 @@ import CalendarIconBasic from './calendar-icon-basic'; import MobileStoreIconBasic from './mobile-store-icon-basic'; import BalanceIconBasic from './balance-icon-basic'; import BuildingIconBasic from './building-icon-basic'; +import GlobalIconBasic from './global-icon-basic'; +import UserIconBasic from './user-icon-basic'; +import GlobalSearchIconBasic from './global-search-icon-basic'; +import ToolsIconBasic from './tools-icon-basic'; +import TrophyIconBasic from './trophy-icon-basic'; +import MicrophoneIconBasic from './microphone-icon-basic'; +import LikeIconBasic from './like-icon-basic'; +import BellIconBasic from './bell-icon-basic'; +import HeartIconBasic from './heart-icon-basic'; +import TrashIconBasic from './trash-icon-basic'; +import ShareIconBasic from './share-icon-basic'; +import PaperPlaneIconBasic from './paper-plane-icon-basic'; +import MailStackIconBasic from './mail-stack-icon-basic'; export type IconDefinition = { name: string; @@ -153,4 +179,107 @@ export const AVAILABLE_ICONS: IconDefinition[] = [ example: , filePath: 'app/docs/icons/building-icon-basic.tsx', }, + { + name: 'Global Icon', + installName: 'global-icon', + component: , + example: , + filePath: 'app/docs/icons/global-icon-basic.tsx', + }, + { + name: 'User Icon', + installName: 'user-icon', + component: , + example: , + filePath: 'app/docs/icons/user-icon-basic.tsx', + }, + { + name: 'Global Search Icon', + installName: 'global-search-icon', + component: ( + + ), + example: , + filePath: 'app/docs/icons/global-search-icon-basic.tsx', + }, + { + name: 'Tools Icon', + installName: 'tools-icon', + component: ( + + ), + example: , + filePath: 'app/docs/icons/tools-icon-basic.tsx', + }, + { + name: 'Trophy Icon', + installName: 'trophy-icon', + component: , + example: , + filePath: 'app/docs/icons/trophy-icon-basic.tsx', + }, + { + name: 'Microphone Icon', + installName: 'microphone-icon', + component: ( + + ), + example: , + filePath: 'app/docs/icons/microphone-icon-basic.tsx', + }, + { + name: 'Like Icon', + installName: 'like-icon', + component: , + example: , + filePath: 'app/docs/icons/like-icon-basic.tsx', + }, + { + name: 'Bell Icon', + installName: 'bell-icon', + component: ( + + ), + example: , + filePath: 'app/docs/icons/bell-icon-basic.tsx', + }, + { + name: 'Heart Icon', + installName: 'heart-icon', + component: , + example: , + filePath: 'app/docs/icons/heart-icon-basic.tsx', + }, + { + name: 'Trash Icon', + installName: 'trash-icon', + component: , + example: , + filePath: 'app/docs/icons/trash-icon-basic.tsx', + }, + { + name: 'Share Icon', + installName: 'share-icon', + component: , + example: , + filePath: 'app/docs/icons/share-icon-basic.tsx', + }, + { + name: 'Paper Plane Icon', + installName: 'paper-plane-icon', + component: ( + + ), + example: , + filePath: 'app/docs/icons/paper-plane-icon-basic.tsx', + }, + { + name: 'Mail Stack Icon', + installName: 'mail-stack-icon', + component: ( + + ), + example: , + filePath: 'app/docs/icons/mail-stack-icon-basic.tsx', + }, ]; diff --git a/app/docs/icons/bell-icon-basic.tsx b/app/docs/icons/bell-icon-basic.tsx new file mode 100644 index 0000000..69a0963 --- /dev/null +++ b/app/docs/icons/bell-icon-basic.tsx @@ -0,0 +1,25 @@ +'use client'; + +import { BellIcon } from '@/components/core/bell-icon'; + +export default function BellIconBasic() { + return ( +
+
+ + + +
+

+ Hover to see animations. Center shows ringing bell with custom colors. +

+
+ ); +} diff --git a/app/docs/icons/gallery-wrapper.tsx b/app/docs/icons/gallery-wrapper.tsx index f2ea72b..d93367e 100644 --- a/app/docs/icons/gallery-wrapper.tsx +++ b/app/docs/icons/gallery-wrapper.tsx @@ -3,6 +3,6 @@ import { IconGallery } from './icon-gallery'; export async function IconGalleryWrapper() { const icons = await getIconsData(); - // @ts-ignore - Valid React Node passing from Server to Client + return ; } diff --git a/app/docs/icons/global-icon-basic.tsx b/app/docs/icons/global-icon-basic.tsx new file mode 100644 index 0000000..f660c35 --- /dev/null +++ b/app/docs/icons/global-icon-basic.tsx @@ -0,0 +1,25 @@ +'use client'; + +import { GlobalIcon } from '@/components/core/global-icon'; + +export default function GlobalIconBasic() { + return ( +
+
+ + + +
+

+ Hover to see animations. Center shows bouncing pins with custom colors. +

+
+ ); +} diff --git a/app/docs/icons/global-search-icon-basic.tsx b/app/docs/icons/global-search-icon-basic.tsx new file mode 100644 index 0000000..4009cf9 --- /dev/null +++ b/app/docs/icons/global-search-icon-basic.tsx @@ -0,0 +1,25 @@ +'use client'; + +import { GlobalSearchIcon } from '@/components/core/global-search-icon'; + +export default function GlobalSearchIconBasic() { + return ( +
+
+ + + +
+

+ Hover to see animations. Center shows rotating globe with custom colors. +

+
+ ); +} diff --git a/app/docs/icons/heart-icon-basic.tsx b/app/docs/icons/heart-icon-basic.tsx new file mode 100644 index 0000000..5620a82 --- /dev/null +++ b/app/docs/icons/heart-icon-basic.tsx @@ -0,0 +1,25 @@ +'use client'; + +import { HeartIcon } from '@/components/core/heart-icon'; + +export default function HeartIconBasic() { + return ( +
+
+ + + +
+

+ Hover to see animations. Center shows beating heart with custom colors. +

+
+ ); +} diff --git a/app/docs/icons/like-icon-basic.tsx b/app/docs/icons/like-icon-basic.tsx new file mode 100644 index 0000000..ee8d09b --- /dev/null +++ b/app/docs/icons/like-icon-basic.tsx @@ -0,0 +1,26 @@ +'use client'; + +import { LikeIcon } from '@/components/core/like-icon'; + +export default function LikeIconBasic() { + return ( +
+
+ + + +
+

+ Hover to see animations. Center shows "like" reaction with + custom colors. +

+
+ ); +} diff --git a/app/docs/icons/mail-stack-icon-basic.tsx b/app/docs/icons/mail-stack-icon-basic.tsx new file mode 100644 index 0000000..055d63d --- /dev/null +++ b/app/docs/icons/mail-stack-icon-basic.tsx @@ -0,0 +1,25 @@ +'use client'; + +import { MailStackIcon } from '@/components/core/mail-stack-icon'; + +export default function MailStackIconBasic() { + return ( +
+
+ + + +
+

+ Hover to see animations. Center shows sliding stack with custom colors. +

+
+ ); +} diff --git a/app/docs/icons/microphone-icon-basic.tsx b/app/docs/icons/microphone-icon-basic.tsx new file mode 100644 index 0000000..09878bf --- /dev/null +++ b/app/docs/icons/microphone-icon-basic.tsx @@ -0,0 +1,25 @@ +'use client'; + +import { MicrophoneIcon } from '@/components/core/microphone-icon'; + +export default function MicrophoneIconBasic() { + return ( +
+
+ + + +
+

+ Hover to see animations. Center shows shaking mic with custom colors. +

+
+ ); +} diff --git a/app/docs/icons/paper-plane-icon-basic.tsx b/app/docs/icons/paper-plane-icon-basic.tsx new file mode 100644 index 0000000..af25677 --- /dev/null +++ b/app/docs/icons/paper-plane-icon-basic.tsx @@ -0,0 +1,25 @@ +'use client'; + +import { PaperPlaneIcon } from '@/components/core/paper-plane-icon'; + +export default function PaperPlaneIconBasic() { + return ( +
+
+ + + +
+

+ Hover to see animations. Center shows flying plane with custom colors. +

+
+ ); +} diff --git a/app/docs/icons/phone-icon-basic.tsx b/app/docs/icons/phone-icon-basic.tsx index a9bd945..8d982c1 100644 --- a/app/docs/icons/phone-icon-basic.tsx +++ b/app/docs/icons/phone-icon-basic.tsx @@ -1,7 +1,6 @@ 'use client'; import { PhoneIcon } from '@/components/core/phone-icon'; -import { useState } from 'react'; export default function PhoneIconBasic() { return ( diff --git a/app/docs/icons/share-icon-basic.tsx b/app/docs/icons/share-icon-basic.tsx new file mode 100644 index 0000000..87c32d5 --- /dev/null +++ b/app/docs/icons/share-icon-basic.tsx @@ -0,0 +1,25 @@ +'use client'; + +import { ShareIcon } from '@/components/core/share-icon'; + +export default function ShareIconBasic() { + return ( +
+
+ + + +
+

+ Hover to see animations. Center shows rotating graph with custom colors. +

+
+ ); +} diff --git a/app/docs/icons/tools-icon-basic.tsx b/app/docs/icons/tools-icon-basic.tsx new file mode 100644 index 0000000..539388b --- /dev/null +++ b/app/docs/icons/tools-icon-basic.tsx @@ -0,0 +1,26 @@ +'use client'; + +import { ToolsIcon } from '@/components/core/tools-icon'; + +export default function ToolsIconBasic() { + return ( +
+
+ + + +
+

+ Hover to see animations. Center shows "repair" with custom + colors. +

+
+ ); +} diff --git a/app/docs/icons/trash-icon-basic.tsx b/app/docs/icons/trash-icon-basic.tsx new file mode 100644 index 0000000..98b7996 --- /dev/null +++ b/app/docs/icons/trash-icon-basic.tsx @@ -0,0 +1,25 @@ +'use client'; + +import { TrashIcon } from '@/components/core/trash-icon'; + +export default function TrashIconBasic() { + return ( +
+
+ + + +
+

+ Hover to see animations. Center shows lid flipping with custom colors. +

+
+ ); +} diff --git a/app/docs/icons/trophy-icon-basic.tsx b/app/docs/icons/trophy-icon-basic.tsx new file mode 100644 index 0000000..813091d --- /dev/null +++ b/app/docs/icons/trophy-icon-basic.tsx @@ -0,0 +1,25 @@ +'use client'; + +import { TrophyIcon } from '@/components/core/trophy-icon'; + +export default function TrophyIconBasic() { + return ( +
+
+ + + +
+

+ Hover to see animations. Center shows pulsing star with custom colors. +

+
+ ); +} diff --git a/app/docs/icons/user-icon-basic.tsx b/app/docs/icons/user-icon-basic.tsx new file mode 100644 index 0000000..37fa87b --- /dev/null +++ b/app/docs/icons/user-icon-basic.tsx @@ -0,0 +1,26 @@ +'use client'; + +import { UserIcon } from '@/components/core/user-icon'; + +export default function UserIconBasic() { + return ( +
+
+ + + +
+

+ Hover to see animations. Center shows "hi" wave with custom + colors. +

+
+ ); +} diff --git a/cli/package-lock.json b/cli/package-lock.json index 6b15ca6..434b415 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -1,12 +1,12 @@ { "name": "shadcn-extras", - "version": "0.3.11", + "version": "0.3.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "shadcn-extras", - "version": "0.3.11", + "version": "0.3.12", "license": "MIT", "dependencies": { "commander": "^9.5.0", diff --git a/cli/package.json b/cli/package.json index 24d1964..903db0e 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "shadcn-extras", - "version": "0.3.11", + "version": "0.3.12", "description": "CLI to add shadcn-extras components to your app", "bin": { "shadcn-extras": "./dist/index.js" diff --git a/components/core/bell-icon.tsx b/components/core/bell-icon.tsx new file mode 100644 index 0000000..3e5710e --- /dev/null +++ b/components/core/bell-icon.tsx @@ -0,0 +1,146 @@ +'use client'; + +import { motion, useAnimation } from 'motion/react'; +import { cn } from '@/lib/utils'; +import { useEffect, useState } from 'react'; + +interface BellIconProps { + className?: string; + color?: string; // Bell Body color + clapperColor?: string; // Clapper/Ball color + size?: number; + isAnimating?: boolean; + startOnHover?: boolean; + animationType?: 'bounce' | 'ring' | 'shake'; +} + +export function BellIcon({ + className, + color = 'currentColor', // Bell Body + clapperColor, + size = 24, + isAnimating = false, + startOnHover = true, + animationType = 'bounce', +}: BellIconProps) { + const controls = useAnimation(); + const clapperControls = useAnimation(); + const [isHovered, setIsHovered] = useState(false); + const shouldAnimate = isAnimating || (startOnHover && isHovered); + + const finalClapperColor = clapperColor || color; + + useEffect(() => { + if (shouldAnimate) { + if (animationType === 'bounce') { + controls.start({ + y: -4, + transition: { + duration: 0.5, + repeat: Infinity, + repeatType: 'reverse', + ease: 'easeInOut', + }, + }); + clapperControls.start({ + y: -4, + transition: { + duration: 0.5, + repeat: Infinity, + repeatType: 'reverse', + ease: 'easeInOut', + }, + }); + } else if (animationType === 'ring') { + // Swing the bell body + controls.start({ + rotate: [0, -15, 15, -10, 10, 0], + transition: { + duration: 1.5, + repeat: Infinity, + ease: 'easeInOut', + originX: '12px', + originY: '4px', // Pivot somewhat high + }, + }); + // Clapper swings opposite or with delay for realism + clapperControls.start({ + x: [0, 2, -2, 1, -1, 0], + transition: { + duration: 1.5, + repeat: Infinity, + ease: 'easeInOut', + }, + }); + } else if (animationType === 'shake') { + controls.start({ + x: [-2, 2, -2, 2, 0], + transition: { + duration: 0.4, + repeat: Infinity, + repeatDelay: 1, + ease: 'easeInOut', + }, + }); + clapperControls.start({ + x: [-2, 2, -2, 2, 0], + transition: { + duration: 0.4, + repeat: Infinity, + repeatDelay: 1, + ease: 'easeInOut', + }, + }); + } + } else { + controls.stop(); + controls.set({ y: 0, rotate: 0, x: 0 }); + clapperControls.stop(); + clapperControls.set({ y: 0, x: 0 }); + } + }, [shouldAnimate, animationType, controls, clapperControls]); + + return ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + + {/* Bell Body Group */} + + {/* Top Loop/Handle */} + + {/* Bell Dome */} + + + + {/* Clapper (Ball) */} + + + + +
+ ); +} diff --git a/components/core/building-icon.tsx b/components/core/building-icon.tsx index 67fee10..5aeb50c 100644 --- a/components/core/building-icon.tsx +++ b/components/core/building-icon.tsx @@ -86,7 +86,7 @@ export function BuildingIcon({ }); } else if (animationType === 'lights') { // Flicker lights - lightControls.start((i) => ({ + lightControls.start(() => ({ opacity: [1, 0.3, 1], transition: { duration: Math.random() * 1 + 0.5, diff --git a/components/core/global-icon.tsx b/components/core/global-icon.tsx new file mode 100644 index 0000000..dc82f7a --- /dev/null +++ b/components/core/global-icon.tsx @@ -0,0 +1,139 @@ +'use client'; + +import { motion, useAnimation } from 'motion/react'; +import { cn } from '@/lib/utils'; +import { useEffect, useState } from 'react'; + +interface GlobalIconProps { + className?: string; + color?: string; // Globe color + pinColor?: string; // Pin color + size?: number; + isAnimating?: boolean; + startOnHover?: boolean; + animationType?: 'spin' | 'bounce' | 'ping'; +} + +export function GlobalIcon({ + className, + color = 'currentColor', + pinColor, + size = 24, + isAnimating = false, + startOnHover = true, + animationType = 'bounce', +}: GlobalIconProps) { + const controls = useAnimation(); + const pinControls = useAnimation(); // For pins + const [isHovered, setIsHovered] = useState(false); + const shouldAnimate = isAnimating || (startOnHover && isHovered); + + const finalPinColor = pinColor || color; + + useEffect(() => { + if (shouldAnimate) { + if (animationType === 'spin') { + controls.start({ + rotate: 360, + transition: { + duration: 8, + repeat: Infinity, + ease: 'linear', + }, + }); + } else if (animationType === 'bounce') { + pinControls.start({ + y: [0, -5, 0], + transition: { + duration: 1, + repeat: Infinity, + ease: 'easeInOut', + }, + }); + } else if (animationType === 'ping') { + pinControls.start({ + scale: [1, 1.2, 1], + opacity: [1, 0.7, 1], + transition: { + duration: 1.5, + repeat: Infinity, + ease: 'easeInOut', + }, + }); + } + } else { + controls.stop(); + controls.set({ rotate: 0 }); + pinControls.stop(); + pinControls.set({ y: 0, scale: 1, opacity: 1 }); + } + }, [shouldAnimate, animationType, controls, pinControls]); + + return ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + + {/* Globe Group */} + + + + + + + {/* Pin 1 - Top Left */} + + {/* Pin Shape */} + + {/* Pin Dot */} + + + + {/* Pin 2 - Bottom Right */} + + {/* Pin Shape - Scaled/Positioned at 19,19 approx */} + + + + + + +
+ ); +} diff --git a/components/core/global-search-icon.tsx b/components/core/global-search-icon.tsx new file mode 100644 index 0000000..f610386 --- /dev/null +++ b/components/core/global-search-icon.tsx @@ -0,0 +1,129 @@ +'use client'; + +import { motion, useAnimation } from 'motion/react'; +import { cn } from '@/lib/utils'; +import { useEffect, useState } from 'react'; + +interface GlobalSearchIconProps { + className?: string; + color?: string; // Glass Frame color + globeColor?: string; // Globe color + size?: number; + isAnimating?: boolean; + startOnHover?: boolean; + animationType?: 'rotate' | 'search' | 'shake'; +} + +export function GlobalSearchIcon({ + className, + color = 'currentColor', // Black frame typically + globeColor, + size = 24, + isAnimating = false, + startOnHover = true, + animationType = 'rotate', +}: GlobalSearchIconProps) { + const controls = useAnimation(); + const globeControls = useAnimation(); // For the inner globe + const [isHovered, setIsHovered] = useState(false); + const shouldAnimate = isAnimating || (startOnHover && isHovered); + + const finalGlobeColor = globeColor || color; + + useEffect(() => { + if (shouldAnimate) { + if (animationType === 'rotate') { + globeControls.start({ + rotate: 360, + transition: { + duration: 4, + repeat: Infinity, + ease: 'linear', + }, + }); + } else if (animationType === 'search') { + controls.start({ + x: [0, 5, -5, 0, 5, -5, 0], + y: [0, 5, 5, 0, -5, -5, 0], + transition: { + duration: 2, + repeat: Infinity, + ease: 'easeInOut', + }, + }); + } else if (animationType === 'shake') { + controls.start({ + x: [0, -3, 3, -3, 3, 0], + transition: { + duration: 0.5, + repeat: Infinity, + repeatDelay: 1, + ease: 'easeInOut', + }, + }); + } + } else { + controls.stop(); + controls.set({ x: 0, y: 0 }); + globeControls.stop(); + globeControls.set({ rotate: 0 }); + } + }, [shouldAnimate, animationType, controls, globeControls]); + + return ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + + {/* Magnifying Glass Handle */} + + + {/* Magnifying Glass Ring */} + + + {/* Globe Inside - Masked or just placed carefully inside */} + + + + + + + +
+ ); +} diff --git a/components/core/heart-icon.tsx b/components/core/heart-icon.tsx new file mode 100644 index 0000000..2916fad --- /dev/null +++ b/components/core/heart-icon.tsx @@ -0,0 +1,109 @@ +'use client'; + +import { motion, useAnimation } from 'motion/react'; +import { cn } from '@/lib/utils'; +import { useEffect, useState } from 'react'; + +interface HeartIconProps { + className?: string; + color?: string; // Heart outline color + shineColor?: string; // Shine/Reflection color + size?: number; + isAnimating?: boolean; + startOnHover?: boolean; + animationType?: 'bounce' | 'beat' | 'pulse'; +} + +export function HeartIcon({ + className, + color = 'currentColor', // Black typically + shineColor, + size = 24, + isAnimating = false, + startOnHover = true, + animationType = 'bounce', +}: HeartIconProps) { + const controls = useAnimation(); + const [isHovered, setIsHovered] = useState(false); + const shouldAnimate = isAnimating || (startOnHover && isHovered); + + const finalShineColor = shineColor || color; + + useEffect(() => { + if (shouldAnimate) { + if (animationType === 'bounce') { + controls.start({ + y: -5, + transition: { + duration: 0.5, + repeat: Infinity, + repeatType: 'reverse', + ease: 'easeInOut', + }, + }); + } else if (animationType === 'beat') { + // Classic heartbeat: thump-thump... thump-thump... + controls.start({ + scale: [1, 1.2, 1, 1.2, 1], + transition: { + duration: 1, // 1 second for the double beat + repeat: Infinity, + repeatDelay: 0.5, // Pause between beats + ease: 'easeInOut', + }, + }); + } else if (animationType === 'pulse') { + controls.start({ + scale: [1, 1.1, 1], + transition: { + duration: 1.5, + repeat: Infinity, + ease: 'easeInOut', + }, + }); + } + } else { + controls.stop(); + controls.set({ y: 0, scale: 1 }); + } + }, [shouldAnimate, animationType, controls]); + + return ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + + + {/* Heart Shape */} + + {/* Shine - Top Right Lobe */} + + + +
+ ); +} diff --git a/components/core/like-icon.tsx b/components/core/like-icon.tsx new file mode 100644 index 0000000..f134fff --- /dev/null +++ b/components/core/like-icon.tsx @@ -0,0 +1,122 @@ +'use client'; + +import { motion, useAnimation } from 'motion/react'; +import { cn } from '@/lib/utils'; +import { useEffect, useState } from 'react'; + +interface LikeIconProps { + className?: string; + color?: string; // Hand color + cuffColor?: string; // Cuff/Sleeve color + size?: number; + isAnimating?: boolean; + startOnHover?: boolean; + animationType?: 'bounce' | 'like' | 'wiggle'; +} + +export function LikeIcon({ + className, + color = 'currentColor', // Black typically + cuffColor, + size = 24, + isAnimating = false, + startOnHover = true, + animationType = 'bounce', +}: LikeIconProps) { + const controls = useAnimation(); + const thumbControls = useAnimation(); + const [isHovered, setIsHovered] = useState(false); + const shouldAnimate = isAnimating || (startOnHover && isHovered); + + const finalCuffColor = cuffColor || color; + + useEffect(() => { + if (shouldAnimate) { + if (animationType === 'bounce') { + controls.start({ + y: -5, + transition: { + duration: 0.5, + repeat: Infinity, + repeatType: 'reverse', + ease: 'easeInOut', + }, + }); + } else if (animationType === 'like') { + controls.start({ + scale: [1, 1.2, 1], + rotate: [0, -15, 0], + transition: { + duration: 0.6, + repeat: Infinity, + repeatDelay: 1, + ease: 'easeInOut', + }, + }); + } else if (animationType === 'wiggle') { + // Wiggle the thumb part specifically if possible, or the whole hand + thumbControls.start({ + rotate: [0, -15, 15, -10, 10, 0], + originX: '6px', + originY: '14px', // Approximate pivot for thumb + transition: { + duration: 1, + repeat: Infinity, + ease: 'easeInOut', + }, + }); + } + } else { + controls.stop(); + controls.set({ y: 0, scale: 1, rotate: 0 }); + thumbControls.stop(); + thumbControls.set({ rotate: 0 }); + } + }, [shouldAnimate, animationType, controls, thumbControls]); + + return ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + + + {/* Cuff / Sleeve */} + + + {/* Hand Main Body (Fingers) */} + + + {/* Thumb */} + + + +
+ ); +} diff --git a/components/core/mail-stack-icon.tsx b/components/core/mail-stack-icon.tsx new file mode 100644 index 0000000..28294b5 --- /dev/null +++ b/components/core/mail-stack-icon.tsx @@ -0,0 +1,147 @@ +'use client'; + +import { motion, useAnimation } from 'motion/react'; +import { cn } from '@/lib/utils'; +import { useEffect, useState } from 'react'; + +interface MailStackIconProps { + className?: string; + color?: string; // Front envelope color + stackColor?: string; // Background stack color + size?: number; + isAnimating?: boolean; + startOnHover?: boolean; + animationType?: 'bounce' | 'slide' | 'rotate'; +} + +export function MailStackIcon({ + className, + color = 'currentColor', // Front envelope + stackColor, + size = 24, + isAnimating = false, + startOnHover = true, + animationType = 'bounce', +}: MailStackIconProps) { + const controls = useAnimation(); + const stackControls = useAnimation(); + const [isHovered, setIsHovered] = useState(false); + const shouldAnimate = isAnimating || (startOnHover && isHovered); + + const finalStackColor = stackColor || color; + + useEffect(() => { + if (shouldAnimate) { + if (animationType === 'bounce') { + controls.start({ + y: -4, + transition: { + duration: 0.5, + repeat: Infinity, + repeatType: 'reverse', + ease: 'easeInOut', + }, + }); + stackControls.start({ + y: -4, + transition: { + duration: 0.5, + repeat: Infinity, + repeatType: 'reverse', + ease: 'easeInOut', + delay: 0.1, // Stagger effect + }, + }); + } else if (animationType === 'slide') { + stackControls.start({ + x: [0, 4, 0], + y: [0, -4, 0], + transition: { + duration: 1.5, + repeat: Infinity, + ease: 'easeInOut', + }, + }); + } else if (animationType === 'rotate') { + stackControls.start({ + rotate: [0, -10, 0], + transition: { + duration: 2, + repeat: Infinity, + ease: 'easeInOut', + originX: '12px', + originY: '12px', + }, + }); + controls.start({ + rotate: [0, 5, 0], + transition: { + duration: 2, + repeat: Infinity, + ease: 'easeInOut', + originX: '12px', + originY: '12px', + }, + }); + } + } else { + controls.stop(); + controls.set({ x: 0, y: 0, rotate: 0 }); + stackControls.stop(); + stackControls.set({ x: 0, y: 0, rotate: 0 }); + } + }, [shouldAnimate, animationType, controls, stackControls]); + + return ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + + {/* Background Stack 2 (Furthest) */} + + + {/* Background Stack 1 (Middle) */} + + + {/* Front Envelope */} + + + + + +
+ ); +} diff --git a/components/core/microphone-icon.tsx b/components/core/microphone-icon.tsx new file mode 100644 index 0000000..707eaaf --- /dev/null +++ b/components/core/microphone-icon.tsx @@ -0,0 +1,128 @@ +'use client'; + +import { motion, useAnimation } from 'motion/react'; +import { cn } from '@/lib/utils'; +import { useEffect, useState } from 'react'; + +interface MicrophoneIconProps { + className?: string; + color?: string; // Mic body color + standColor?: string; // Stand/Base color + size?: number; + isAnimating?: boolean; + startOnHover?: boolean; + animationType?: 'bounce' | 'shake' | 'pulse'; +} + +export function MicrophoneIcon({ + className, + color = 'currentColor', // Mic body + standColor, + size = 24, + isAnimating = false, + startOnHover = true, + animationType = 'bounce', +}: MicrophoneIconProps) { + const controls = useAnimation(); + const standControls = useAnimation(); + const [isHovered, setIsHovered] = useState(false); + const shouldAnimate = isAnimating || (startOnHover && isHovered); + + const finalStandColor = standColor || color; + + useEffect(() => { + if (shouldAnimate) { + if (animationType === 'bounce') { + controls.start({ + y: -4, + transition: { + duration: 0.5, + repeat: Infinity, + repeatType: 'reverse', + ease: 'easeInOut', + }, + }); + standControls.start({ + y: -4, + transition: { + duration: 0.5, + repeat: Infinity, + repeatType: 'reverse', + ease: 'easeInOut', + }, + }); + } else if (animationType === 'shake') { + controls.start({ + x: [-2, 2, -2, 2, 0], + transition: { + duration: 0.4, + repeat: Infinity, + repeatDelay: 1, // Periodic shake like voice input + ease: 'easeInOut', + }, + }); + } else if (animationType === 'pulse') { + controls.start({ + scale: [1, 1.1, 1], + opacity: [1, 0.8, 1], + transition: { + duration: 1.5, + repeat: Infinity, + ease: 'easeInOut', + }, + }); + } + } else { + controls.stop(); + controls.set({ y: 0, x: 0, scale: 1, opacity: 1 }); + standControls.stop(); + standControls.set({ y: 0 }); + } + }, [shouldAnimate, animationType, controls, standControls]); + + return ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + + {/* Mic Body */} + + + + + + + + {/* Mic Stand */} + + {/* U-Shape Holder */} + + {/* Stem */} + + {/* Base */} + + + +
+ ); +} diff --git a/components/core/network-icon.tsx b/components/core/network-icon.tsx index a4f9049..d6acf2a 100644 --- a/components/core/network-icon.tsx +++ b/components/core/network-icon.tsx @@ -29,7 +29,6 @@ export function NetworkIcon({ // Calculate sizes relative to the main size const userSize = size * 0.5; const nodeSize = size * 0.15; - const radius = size * 0.35; // Distance from center to nodes useEffect(() => { if (shouldAnimate) { diff --git a/components/core/paper-plane-icon.tsx b/components/core/paper-plane-icon.tsx new file mode 100644 index 0000000..a309355 --- /dev/null +++ b/components/core/paper-plane-icon.tsx @@ -0,0 +1,136 @@ +'use client'; + +import { motion, useAnimation } from 'motion/react'; +import { cn } from '@/lib/utils'; +import { useEffect, useState } from 'react'; + +interface PaperPlaneIconProps { + className?: string; + color?: string; // Plane color + trailColor?: string; // Trail color + size?: number; + isAnimating?: boolean; + startOnHover?: boolean; + animationType?: 'bounce' | 'fly' | 'wobble'; +} + +export function PaperPlaneIcon({ + className, + color = 'currentColor', // Plane + trailColor, + size = 24, + isAnimating = false, + startOnHover = true, + animationType = 'bounce', +}: PaperPlaneIconProps) { + const controls = useAnimation(); + const trailControls = useAnimation(); + const [isHovered, setIsHovered] = useState(false); + const shouldAnimate = isAnimating || (startOnHover && isHovered); + + const finalTrailColor = trailColor || color; + + useEffect(() => { + if (shouldAnimate) { + if (animationType === 'bounce') { + controls.start({ + y: -5, + transition: { + duration: 0.5, + repeat: Infinity, + repeatType: 'reverse', + ease: 'easeInOut', + }, + }); + trailControls.start({ + opacity: [0, 1, 0], + transition: { + duration: 1, + repeat: Infinity, + ease: 'easeInOut', + }, + }); + } else if (animationType === 'fly') { + controls.start({ + x: [0, 20], + y: [0, -20], + opacity: [1, 0], + transition: { + duration: 1, + repeat: Infinity, + ease: 'easeIn', + }, + }); + trailControls.start({ + opacity: [1, 0], + pathLength: [0, 1], + transition: { + duration: 1, + repeat: Infinity, + ease: 'easeIn', + }, + }); + } else if (animationType === 'wobble') { + controls.start({ + rotate: [0, -10, 10, -5, 5, 0], + transition: { + duration: 2, + repeat: Infinity, + ease: 'easeInOut', + originX: '12px', + originY: '12px', + }, + }); + } + } else { + controls.stop(); + controls.set({ x: 0, y: 0, opacity: 1, rotate: 0 }); + trailControls.stop(); + trailControls.set({ opacity: 1, pathLength: 1 }); + } + }, [shouldAnimate, animationType, controls, trailControls]); + + return ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + + {/* Trail - Dashed line behind */} + + + + {/* Paper Plane */} + + {/* Main Body */} + + + + +
+ ); +} diff --git a/components/core/share-icon.tsx b/components/core/share-icon.tsx new file mode 100644 index 0000000..8f50508 --- /dev/null +++ b/components/core/share-icon.tsx @@ -0,0 +1,151 @@ +'use client'; + +import { motion, useAnimation } from 'motion/react'; +import { cn } from '@/lib/utils'; +import { useEffect, useState } from 'react'; + +interface ShareIconProps { + className?: string; + color?: string; // Lines/Connection color + dotColor?: string; // Dots color + size?: number; + isAnimating?: boolean; + startOnHover?: boolean; + animationType?: 'bounce' | 'pulse' | 'rotate'; +} + +export function ShareIcon({ + className, + color = 'currentColor', // Lines + dotColor, + size = 24, + isAnimating = false, + startOnHover = true, + animationType = 'bounce', +}: ShareIconProps) { + const controls = useAnimation(); + const dotControls = useAnimation(); + const [isHovered, setIsHovered] = useState(false); + const shouldAnimate = isAnimating || (startOnHover && isHovered); + + const finalDotColor = dotColor || color; + + useEffect(() => { + if (shouldAnimate) { + if (animationType === 'bounce') { + controls.start({ + y: -5, + transition: { + duration: 0.5, + repeat: Infinity, + repeatType: 'reverse', + ease: 'easeInOut', + }, + }); + } else if (animationType === 'pulse') { + dotControls.start({ + scale: [1, 1.3, 1], + transition: { + duration: 1, + repeat: Infinity, + ease: 'easeInOut', + }, + }); + } else if (animationType === 'rotate') { + // Rotate the whole group or just the branches? + // Let's rotate the whole icon for "share" loading effect, or maybe just the right dots around the left one. + // Let's rotate connected dots if possible. + // Actually, simple rotation of the whole icon around the center is easiest and looks "busy". + // But pivoting around the left dot (cx=6, cy=12) is cooler. + controls.start({ + rotate: 360, + transition: { + duration: 2, + repeat: Infinity, + ease: 'linear', + originX: '6px', + originY: '12px', // Pivot around the left dot + }, + }); + } + } else { + controls.stop(); + controls.set({ y: 0, rotate: 0 }); + dotControls.stop(); + dotControls.set({ scale: 1 }); + } + }, [shouldAnimate, animationType, controls, dotControls]); + + return ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + + + {/* Connections */} + {' '} + {/* Anchor point helper if needed */} + + + + + + {/* Re-drawing circles on top if we want dots to pulse separately */} + + + + + +
+ ); +} diff --git a/components/core/tools-icon.tsx b/components/core/tools-icon.tsx new file mode 100644 index 0000000..47c3e48 --- /dev/null +++ b/components/core/tools-icon.tsx @@ -0,0 +1,230 @@ +'use client'; + +import { motion, useAnimation } from 'motion/react'; +import { cn } from '@/lib/utils'; +import { useEffect, useState } from 'react'; + +interface ToolsIconProps { + className?: string; + color?: string; // Wrench color (Primary) + screwdriverColor?: string; // Screwdriver color (Secondary) + size?: number; + isAnimating?: boolean; + startOnHover?: boolean; + animationType?: 'bounce' | 'wiggle' | 'repair'; +} + +export function ToolsIcon({ + className, + color = 'currentColor', // Wrench + screwdriverColor, + size = 24, + isAnimating = false, + startOnHover = true, + animationType = 'bounce', +}: ToolsIconProps) { + const wrenchControls = useAnimation(); + const screwdriverControls = useAnimation(); + const [isHovered, setIsHovered] = useState(false); + const shouldAnimate = isAnimating || (startOnHover && isHovered); + + const finalScrewdriverColor = screwdriverColor || color; + + useEffect(() => { + if (shouldAnimate) { + if (animationType === 'bounce') { + wrenchControls.start({ + y: -4, + transition: { + duration: 0.5, + repeat: Infinity, + repeatType: 'reverse', + ease: 'easeInOut', + }, + }); + screwdriverControls.start({ + y: -4, + transition: { + duration: 0.5, + repeat: Infinity, + repeatType: 'reverse', + ease: 'easeInOut', + delay: 0.1, + }, + }); + } else if (animationType === 'wiggle') { + wrenchControls.start({ + rotate: [0, -10, 10, -5, 5, 0], + transition: { duration: 1.5, repeat: Infinity, ease: 'linear' }, + }); + screwdriverControls.start({ + rotate: [0, 10, -10, 5, -5, 0], + transition: { + duration: 1.5, + repeat: Infinity, + ease: 'linear', + delay: 0.1, + }, + }); + } else if (animationType === 'repair') { + // Pivot movement + wrenchControls.start({ + rotate: [0, -20, 0, -10, 0], + transition: { duration: 2, repeat: Infinity, ease: 'easeInOut' }, + }); + screwdriverControls.start({ + rotate: [0, 20, 0, 10, 0], + transition: { + duration: 2, + repeat: Infinity, + ease: 'easeInOut', + delay: 0.2, + }, + }); + } + } else { + wrenchControls.stop(); + wrenchControls.set({ y: 0, rotate: 0 }); + screwdriverControls.stop(); + screwdriverControls.set({ y: 0, rotate: 0 }); + } + }, [shouldAnimate, animationType, wrenchControls, screwdriverControls]); + + return ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + + {/* Screwdriver: Handle Bottom Left, Tip Top Right */} + + {/* Handle */} + + + {/* Shaft */} + + {/* Tip */} + + + + {/* Wrench: Handle Bottom Right, Head Top Left */} + + {/* Handle */} + + {/* Head */} + + + + + + {/* Re-doing paths to be simpler and cleaner standard crossed tools */} + {/* Using known good path data logic */} + + + {/* Screwdriver */} + + {/* Simple lines for screwdriver is tricky, let's use the Lucide one but oriented */} + + + + + + + + {/* Wrench */} + + + + + {/* FINAL CLEAN VERSION: Overwriting the whole SVG content with verified paths */} + + {/* Screwdriver (Back) */} + + + + + + + + {/* Wrench (Front) */} + + + + + +
+ ); +} diff --git a/components/core/trash-icon.tsx b/components/core/trash-icon.tsx new file mode 100644 index 0000000..2d0a9a7 --- /dev/null +++ b/components/core/trash-icon.tsx @@ -0,0 +1,144 @@ +'use client'; + +import { motion, useAnimation } from 'motion/react'; +import { cn } from '@/lib/utils'; +import { useEffect, useState } from 'react'; + +interface TrashIconProps { + className?: string; + color?: string; // Bin color + lidColor?: string; // Lid color + size?: number; + isAnimating?: boolean; + startOnHover?: boolean; + animationType?: 'bounce' | 'trash' | 'shake'; +} + +export function TrashIcon({ + className, + color = 'currentColor', // Black typically + lidColor, + size = 24, + isAnimating = false, + startOnHover = true, + animationType = 'bounce', +}: TrashIconProps) { + const controls = useAnimation(); + const lidControls = useAnimation(); + const [isHovered, setIsHovered] = useState(false); + const shouldAnimate = isAnimating || (startOnHover && isHovered); + + const finalLidColor = lidColor || color; + + useEffect(() => { + if (shouldAnimate) { + if (animationType === 'bounce') { + controls.start({ + y: -5, + transition: { + duration: 0.5, + repeat: Infinity, + repeatType: 'reverse', + ease: 'easeInOut', + }, + }); + lidControls.start({ + y: -5, + transition: { + duration: 0.5, + repeat: Infinity, + repeatType: 'reverse', + ease: 'easeInOut', + }, + }); + } else if (animationType === 'trash') { + // Lid flip animation + lidControls.start({ + rotate: [0, -45, 0], + originX: '2px', + originY: '5px', // Pivot at left/bottom of lid + transition: { + duration: 0.8, + repeat: Infinity, + repeatDelay: 0.5, + ease: 'easeInOut', + }, + }); + } else if (animationType === 'shake') { + controls.start({ + x: [-2, 2, -2, 2, 0], + transition: { + duration: 0.5, + repeat: Infinity, + repeatDelay: 1, + ease: 'easeInOut', + }, + }); + lidControls.start({ + x: [-2, 2, -2, 2, 0], + transition: { + duration: 0.5, + repeat: Infinity, + repeatDelay: 1, + ease: 'easeInOut', + }, + }); + } + } else { + controls.stop(); + controls.set({ y: 0, x: 0 }); + lidControls.stop(); + lidControls.set({ y: 0, x: 0, rotate: 0 }); + } + }, [shouldAnimate, animationType, controls, lidControls]); + + return ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + + {/* Bin Body */} + + {' '} + {/* Spacer/invisible line if needed, or just rely on following paths */} + + + + + + {/* Lid Group */} + + + + {/* We duplicate the top line of bin here essentially to make it part of the lid if we want it to move with it, + or we keep the line separated. Usually 'Trash' icon has a solid line across top. + Let's make the line x1=3 x2=21 part of the lid animation for 'trash' flip effect. + */} + + +
+ ); +} diff --git a/components/core/trophy-icon.tsx b/components/core/trophy-icon.tsx new file mode 100644 index 0000000..759065d --- /dev/null +++ b/components/core/trophy-icon.tsx @@ -0,0 +1,126 @@ +'use client'; + +import { motion, useAnimation } from 'motion/react'; +import { cn } from '@/lib/utils'; +import { useEffect, useState } from 'react'; + +interface TrophyIconProps { + className?: string; + color?: string; // Trophy color (Cup + Handles + Base) + starColor?: string; // Star color + size?: number; + isAnimating?: boolean; + startOnHover?: boolean; + animationType?: 'bounce' | 'wobble' | 'shine'; +} + +export function TrophyIcon({ + className, + color = 'currentColor', // Black frame typically + starColor, + size = 24, + isAnimating = false, + startOnHover = true, + animationType = 'bounce', +}: TrophyIconProps) { + const controls = useAnimation(); + const starControls = useAnimation(); + const [isHovered, setIsHovered] = useState(false); + const shouldAnimate = isAnimating || (startOnHover && isHovered); + + const finalStarColor = starColor || color; + + useEffect(() => { + if (shouldAnimate) { + if (animationType === 'bounce') { + controls.start({ + y: -5, + transition: { + duration: 0.5, + repeat: Infinity, + repeatType: 'reverse', + ease: 'easeInOut', + }, + }); + } else if (animationType === 'wobble') { + controls.start({ + rotate: [0, -10, 10, -5, 5, 0], + transition: { + duration: 1.5, + repeat: Infinity, + ease: 'easeInOut', + }, + }); + } else if (animationType === 'shine') { + starControls.start({ + scale: [1, 1.3, 1], + opacity: [1, 0.7, 1], + rotate: [0, 15, -15, 0], + transition: { + duration: 1.5, + repeat: Infinity, + ease: 'easeInOut', + }, + }); + } + } else { + controls.stop(); + controls.set({ y: 0, rotate: 0 }); + starControls.stop(); + starControls.set({ scale: 1, opacity: 1, rotate: 0 }); + } + }, [shouldAnimate, animationType, controls, starControls]); + + return ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + + + {/* Cup */} + + {/* Handles */} + + + {/* Base Stem */} + + {/* Base */} + + + + {/* Star in Center */} + + + + +
+ ); +} diff --git a/components/core/user-icon.tsx b/components/core/user-icon.tsx new file mode 100644 index 0000000..5dd307c --- /dev/null +++ b/components/core/user-icon.tsx @@ -0,0 +1,119 @@ +'use client'; + +import { motion, useAnimation } from 'motion/react'; +import { cn } from '@/lib/utils'; +import { useEffect, useState } from 'react'; + +interface UserIconProps { + className?: string; + color?: string; // Frame color + userColor?: string; // User silhouette color + size?: number; + isAnimating?: boolean; + startOnHover?: boolean; + animationType?: 'bounce' | 'hi' | 'pulse'; +} + +export function UserIcon({ + className, + color = 'currentColor', // Black frame typically + userColor, + size = 24, + isAnimating = false, + startOnHover = true, + animationType = 'bounce', +}: UserIconProps) { + const controls = useAnimation(); + const userControls = useAnimation(); // For the inner user shape + const [isHovered, setIsHovered] = useState(false); + const shouldAnimate = isAnimating || (startOnHover && isHovered); + + const finalUserColor = userColor || color; + + useEffect(() => { + if (shouldAnimate) { + if (animationType === 'bounce') { + controls.start({ + y: -4, + transition: { + duration: 0.5, + repeat: Infinity, + repeatType: 'reverse', + ease: 'easeInOut', + }, + }); + } else if (animationType === 'hi') { + // A generic "wave" or "tilt" animation for the user silhouette + userControls.start({ + rotate: [0, -10, 10, -5, 5, 0], + transition: { + duration: 1.5, + repeat: Infinity, + repeatDelay: 1, // Pause between waves + ease: 'easeInOut', + }, + }); + } else if (animationType === 'pulse') { + userControls.start({ + scale: [1, 1.1, 1], + opacity: [1, 0.8, 1], + transition: { + duration: 2, + repeat: Infinity, + ease: 'easeInOut', + }, + }); + } + } else { + controls.stop(); + controls.set({ y: 0 }); + userControls.stop(); + userControls.set({ rotate: 0, scale: 1, opacity: 1 }); + } + }, [shouldAnimate, animationType, controls, userControls]); + + return ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + + {/* Frame: Outer Circle */} + + + {/* User Silhouette: Head + Body */} + + {/* Head */} + + {/* Body (Shoulders/Torso) */} + + + +
+ ); +} diff --git a/lib/shiki.ts b/lib/shiki.ts index 98075a8..4fdcb7d 100644 --- a/lib/shiki.ts +++ b/lib/shiki.ts @@ -1,6 +1,20 @@ import { bundledLanguages, createHighlighter } from 'shiki/bundle/web'; import { noir } from './custom-theme'; +let highlighterPromise: Promise< + Awaited> +> | null = null; + +async function getHighlighter() { + if (!highlighterPromise) { + highlighterPromise = createHighlighter({ + themes: [noir], + langs: [...Object.keys(bundledLanguages)], + }); + } + return highlighterPromise; +} + export const codeToHtml = async ({ code, lang, @@ -8,10 +22,7 @@ export const codeToHtml = async ({ code: string; lang: string; }) => { - const highlighter = await createHighlighter({ - themes: [noir], - langs: [...Object.keys(bundledLanguages)], - }); + const highlighter = await getHighlighter(); return highlighter.codeToHtml(code, { lang: lang, diff --git a/public/e/bell-icon-basic.json b/public/e/bell-icon-basic.json new file mode 100644 index 0000000..fe542dd --- /dev/null +++ b/public/e/bell-icon-basic.json @@ -0,0 +1,18 @@ +{ + "name": "bell-icon-basic", + "type": "registry:ui", + "componentName": "bell-icon-basic", + "description": "Basic usage of the Bell Icon.", + "files": [ + { + "path": "bell-icon-basic.tsx", + "content": "'use client';\n\nimport { BellIcon } from '@/components/core/bell-icon';\n\nexport default function BellIconBasic() {\n return (\n
\n
\n \n \n \n
\n

\n Hover to see animations. Center shows ringing bell with custom colors.\n

\n
\n );\n}\n", + "type": "registry:component" + }, + { + "path": "components/core/bell-icon.tsx", + "content": "'use client';\n\nimport { motion, useAnimation } from 'motion/react';\nimport { cn } from '@/lib/utils';\nimport { useEffect, useState } from 'react';\n\ninterface BellIconProps {\n className?: string;\n color?: string; // Bell Body color\n clapperColor?: string; // Clapper/Ball color\n size?: number;\n isAnimating?: boolean;\n startOnHover?: boolean;\n animationType?: 'bounce' | 'ring' | 'shake';\n}\n\nexport function BellIcon({\n className,\n color = 'currentColor', // Bell Body\n clapperColor,\n size = 24,\n isAnimating = false,\n startOnHover = true,\n animationType = 'bounce',\n}: BellIconProps) {\n const controls = useAnimation();\n const clapperControls = useAnimation();\n const [isHovered, setIsHovered] = useState(false);\n const shouldAnimate = isAnimating || (startOnHover && isHovered);\n\n const finalClapperColor = clapperColor || color;\n\n useEffect(() => {\n if (shouldAnimate) {\n if (animationType === 'bounce') {\n controls.start({\n y: -4,\n transition: {\n duration: 0.5,\n repeat: Infinity,\n repeatType: 'reverse',\n ease: 'easeInOut',\n },\n });\n clapperControls.start({\n y: -4,\n transition: {\n duration: 0.5,\n repeat: Infinity,\n repeatType: 'reverse',\n ease: 'easeInOut',\n },\n });\n } else if (animationType === 'ring') {\n // Swing the bell body\n controls.start({\n rotate: [0, -15, 15, -10, 10, 0],\n transition: {\n duration: 1.5,\n repeat: Infinity,\n ease: 'easeInOut',\n originX: '12px',\n originY: '4px', // Pivot somewhat high\n },\n });\n // Clapper swings opposite or with delay for realism\n clapperControls.start({\n x: [0, 2, -2, 1, -1, 0],\n transition: {\n duration: 1.5,\n repeat: Infinity,\n ease: 'easeInOut',\n },\n });\n } else if (animationType === 'shake') {\n controls.start({\n x: [-2, 2, -2, 2, 0],\n transition: {\n duration: 0.4,\n repeat: Infinity,\n repeatDelay: 1,\n ease: 'easeInOut',\n },\n });\n clapperControls.start({\n x: [-2, 2, -2, 2, 0],\n transition: {\n duration: 0.4,\n repeat: Infinity,\n repeatDelay: 1,\n ease: 'easeInOut',\n },\n });\n }\n } else {\n controls.stop();\n controls.set({ y: 0, rotate: 0, x: 0 });\n clapperControls.stop();\n clapperControls.set({ y: 0, x: 0 });\n }\n }, [shouldAnimate, animationType, controls, clapperControls]);\n\n return (\n setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n >\n \n {/* Bell Body Group */}\n \n {/* Top Loop/Handle */}\n \n {/* Bell Dome */}\n \n \n\n {/* Clapper (Ball) */}\n \n \n \n \n \n );\n}\n", + "type": "registry:ui" + } + ] +} \ No newline at end of file diff --git a/public/e/building-icon-basic.json b/public/e/building-icon-basic.json index 20fe52e..21cdfe1 100644 --- a/public/e/building-icon-basic.json +++ b/public/e/building-icon-basic.json @@ -11,7 +11,7 @@ }, { "path": "components/core/building-icon.tsx", - "content": "'use client';\n\nimport { motion, useAnimation } from 'motion/react';\nimport { cn } from '@/lib/utils';\nimport { useEffect, useState } from 'react';\n\ninterface BuildingIconProps {\n className?: string;\n color?: string; // Building outline color\n lightColor?: string; // Lit window color\n size?: number;\n isAnimating?: boolean;\n startOnHover?: boolean;\n animationType?: 'grow' | 'lights' | 'bounce';\n}\n\nexport function BuildingIcon({\n className,\n color = 'currentColor',\n lightColor,\n size = 24,\n isAnimating = false,\n startOnHover = true,\n animationType = 'lights',\n}: BuildingIconProps) {\n const controls = useAnimation();\n const lightControls = useAnimation();\n const [isHovered, setIsHovered] = useState(false);\n const shouldAnimate = isAnimating || (startOnHover && isHovered);\n\n const finalLightColor = lightColor || color;\n\n // Window grid positions (approximate for 24x24 viewBox)\n // Center tower is roughly x=7 to x=17 (width 10).\n // Window columns around x=9, 12, 15.\n // Window rows y=8, 11, 14, 17.\n const windows = [\n { x: 9, y: 8, id: 1 },\n { x: 12, y: 8, id: 2 },\n { x: 15, y: 8, id: 3 },\n { x: 9, y: 11, id: 4 },\n { x: 12, y: 11, id: 5 },\n { x: 15, y: 11, id: 6 },\n { x: 9, y: 14, id: 7 },\n { x: 12, y: 14, id: 8 },\n { x: 15, y: 14, id: 9 },\n { x: 9, y: 17, id: 10 },\n { x: 12, y: 17, id: 11 },\n { x: 15, y: 17, id: 12 },\n ];\n\n // Specific windows to light up (based on image pattern or random)\n const defaultLitIndices = [0, 5, 8, 9, 11]; // Just some random ones\n\n useEffect(() => {\n if (shouldAnimate) {\n if (animationType === 'grow') {\n controls.start({\n scaleY: [0, 1],\n opacity: [0, 1],\n transition: {\n duration: 0.8,\n ease: 'easeOut',\n repeat: 1, // Grow once (or loop? usually grow is intro)\n repeatDelay: 2,\n },\n });\n // If we want it to loop the grow effect for demo:\n controls.start({\n scaleY: [0.3, 1, 1, 0.3],\n opacity: [0.5, 1, 1, 0.5],\n transition: {\n duration: 2,\n repeat: Infinity,\n ease: 'easeInOut',\n },\n });\n } else if (animationType === 'bounce') {\n controls.start({\n y: [0, -3, 0],\n transition: {\n duration: 0.6,\n repeat: Infinity,\n repeatDelay: 1,\n },\n });\n } else if (animationType === 'lights') {\n // Flicker lights\n lightControls.start((i) => ({\n opacity: [1, 0.3, 1],\n transition: {\n duration: Math.random() * 1 + 0.5,\n repeat: Infinity,\n repeatType: 'reverse',\n delay: Math.random() * 0.5,\n },\n }));\n }\n } else {\n controls.stop();\n controls.set({ scaleY: 1, y: 0, opacity: 1 });\n lightControls.stop();\n lightControls.set({ opacity: 1 });\n }\n }, [shouldAnimate, animationType, controls, lightControls]);\n\n return (\n setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n >\n \n \n {/* Side Wings */}\n \n \n\n {/* Center Tower */}\n \n {/* Antenna */}\n \n \n \n\n {/* Windows Overlay - Scale with building if bouncing, but we can keep separate if we want just lights */}\n \n {windows.map((w, i) => {\n const isLit = defaultLitIndices.includes(i);\n // Render lit windows with specific color, others with stroke or lighter fill\n return (\n \n );\n })}\n \n \n \n );\n}\n", + "content": "'use client';\n\nimport { motion, useAnimation } from 'motion/react';\nimport { cn } from '@/lib/utils';\nimport { useEffect, useState } from 'react';\n\ninterface BuildingIconProps {\n className?: string;\n color?: string; // Building outline color\n lightColor?: string; // Lit window color\n size?: number;\n isAnimating?: boolean;\n startOnHover?: boolean;\n animationType?: 'grow' | 'lights' | 'bounce';\n}\n\nexport function BuildingIcon({\n className,\n color = 'currentColor',\n lightColor,\n size = 24,\n isAnimating = false,\n startOnHover = true,\n animationType = 'lights',\n}: BuildingIconProps) {\n const controls = useAnimation();\n const lightControls = useAnimation();\n const [isHovered, setIsHovered] = useState(false);\n const shouldAnimate = isAnimating || (startOnHover && isHovered);\n\n const finalLightColor = lightColor || color;\n\n // Window grid positions (approximate for 24x24 viewBox)\n // Center tower is roughly x=7 to x=17 (width 10).\n // Window columns around x=9, 12, 15.\n // Window rows y=8, 11, 14, 17.\n const windows = [\n { x: 9, y: 8, id: 1 },\n { x: 12, y: 8, id: 2 },\n { x: 15, y: 8, id: 3 },\n { x: 9, y: 11, id: 4 },\n { x: 12, y: 11, id: 5 },\n { x: 15, y: 11, id: 6 },\n { x: 9, y: 14, id: 7 },\n { x: 12, y: 14, id: 8 },\n { x: 15, y: 14, id: 9 },\n { x: 9, y: 17, id: 10 },\n { x: 12, y: 17, id: 11 },\n { x: 15, y: 17, id: 12 },\n ];\n\n // Specific windows to light up (based on image pattern or random)\n const defaultLitIndices = [0, 5, 8, 9, 11]; // Just some random ones\n\n useEffect(() => {\n if (shouldAnimate) {\n if (animationType === 'grow') {\n controls.start({\n scaleY: [0, 1],\n opacity: [0, 1],\n transition: {\n duration: 0.8,\n ease: 'easeOut',\n repeat: 1, // Grow once (or loop? usually grow is intro)\n repeatDelay: 2,\n },\n });\n // If we want it to loop the grow effect for demo:\n controls.start({\n scaleY: [0.3, 1, 1, 0.3],\n opacity: [0.5, 1, 1, 0.5],\n transition: {\n duration: 2,\n repeat: Infinity,\n ease: 'easeInOut',\n },\n });\n } else if (animationType === 'bounce') {\n controls.start({\n y: [0, -3, 0],\n transition: {\n duration: 0.6,\n repeat: Infinity,\n repeatDelay: 1,\n },\n });\n } else if (animationType === 'lights') {\n // Flicker lights\n lightControls.start(() => ({\n opacity: [1, 0.3, 1],\n transition: {\n duration: Math.random() * 1 + 0.5,\n repeat: Infinity,\n repeatType: 'reverse',\n delay: Math.random() * 0.5,\n },\n }));\n }\n } else {\n controls.stop();\n controls.set({ scaleY: 1, y: 0, opacity: 1 });\n lightControls.stop();\n lightControls.set({ opacity: 1 });\n }\n }, [shouldAnimate, animationType, controls, lightControls]);\n\n return (\n setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n >\n \n \n {/* Side Wings */}\n \n \n\n {/* Center Tower */}\n \n {/* Antenna */}\n \n \n \n\n {/* Windows Overlay - Scale with building if bouncing, but we can keep separate if we want just lights */}\n \n {windows.map((w, i) => {\n const isLit = defaultLitIndices.includes(i);\n // Render lit windows with specific color, others with stroke or lighter fill\n return (\n \n );\n })}\n \n \n \n );\n}\n", "type": "registry:ui" } ] diff --git a/public/e/global-icon-basic.json b/public/e/global-icon-basic.json new file mode 100644 index 0000000..8b6025a --- /dev/null +++ b/public/e/global-icon-basic.json @@ -0,0 +1,18 @@ +{ + "name": "global-icon-basic", + "type": "registry:ui", + "componentName": "global-icon-basic", + "description": "Basic usage of the Global Icon.", + "files": [ + { + "path": "global-icon-basic.tsx", + "content": "'use client';\n\nimport { GlobalIcon } from '@/components/core/global-icon';\n\nexport default function GlobalIconBasic() {\n return (\n
\n
\n \n \n \n
\n

\n Hover to see animations. Center shows bouncing pins with custom colors.\n

\n
\n );\n}\n", + "type": "registry:component" + }, + { + "path": "components/core/global-icon.tsx", + "content": "'use client';\n\nimport { motion, useAnimation } from 'motion/react';\nimport { cn } from '@/lib/utils';\nimport { useEffect, useState } from 'react';\n\ninterface GlobalIconProps {\n className?: string;\n color?: string; // Globe color\n pinColor?: string; // Pin color\n size?: number;\n isAnimating?: boolean;\n startOnHover?: boolean;\n animationType?: 'spin' | 'bounce' | 'ping';\n}\n\nexport function GlobalIcon({\n className,\n color = 'currentColor',\n pinColor,\n size = 24,\n isAnimating = false,\n startOnHover = true,\n animationType = 'bounce',\n}: GlobalIconProps) {\n const controls = useAnimation();\n const pinControls = useAnimation(); // For pins\n const [isHovered, setIsHovered] = useState(false);\n const shouldAnimate = isAnimating || (startOnHover && isHovered);\n\n const finalPinColor = pinColor || color;\n\n useEffect(() => {\n if (shouldAnimate) {\n if (animationType === 'spin') {\n controls.start({\n rotate: 360,\n transition: {\n duration: 8,\n repeat: Infinity,\n ease: 'linear',\n },\n });\n } else if (animationType === 'bounce') {\n pinControls.start({\n y: [0, -5, 0],\n transition: {\n duration: 1,\n repeat: Infinity,\n ease: 'easeInOut',\n },\n });\n } else if (animationType === 'ping') {\n pinControls.start({\n scale: [1, 1.2, 1],\n opacity: [1, 0.7, 1],\n transition: {\n duration: 1.5,\n repeat: Infinity,\n ease: 'easeInOut',\n },\n });\n }\n } else {\n controls.stop();\n controls.set({ rotate: 0 });\n pinControls.stop();\n pinControls.set({ y: 0, scale: 1, opacity: 1 });\n }\n }, [shouldAnimate, animationType, controls, pinControls]);\n\n return (\n setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n >\n \n {/* Globe Group */}\n \n \n \n \n \n\n {/* Pin 1 - Top Left */}\n \n {/* Pin Shape */}\n \n {/* Pin Dot */}\n \n \n\n {/* Pin 2 - Bottom Right */}\n \n {/* Pin Shape - Scaled/Positioned at 19,19 approx */}\n \n \n \n \n \n \n \n );\n}\n", + "type": "registry:ui" + } + ] +} \ No newline at end of file diff --git a/public/e/global-search-icon-basic.json b/public/e/global-search-icon-basic.json new file mode 100644 index 0000000..94eb857 --- /dev/null +++ b/public/e/global-search-icon-basic.json @@ -0,0 +1,18 @@ +{ + "name": "global-search-icon-basic", + "type": "registry:ui", + "componentName": "global-search-icon-basic", + "description": "Basic usage of the Global Search Icon.", + "files": [ + { + "path": "global-search-icon-basic.tsx", + "content": "'use client';\n\nimport { GlobalSearchIcon } from '@/components/core/global-search-icon';\n\nexport default function GlobalSearchIconBasic() {\n return (\n
\n
\n \n \n \n
\n

\n Hover to see animations. Center shows rotating globe with custom colors.\n

\n
\n );\n}\n", + "type": "registry:component" + }, + { + "path": "components/core/global-search-icon.tsx", + "content": "'use client';\n\nimport { motion, useAnimation } from 'motion/react';\nimport { cn } from '@/lib/utils';\nimport { useEffect, useState } from 'react';\n\ninterface GlobalSearchIconProps {\n className?: string;\n color?: string; // Glass Frame color\n globeColor?: string; // Globe color\n size?: number;\n isAnimating?: boolean;\n startOnHover?: boolean;\n animationType?: 'rotate' | 'search' | 'shake';\n}\n\nexport function GlobalSearchIcon({\n className,\n color = 'currentColor', // Black frame typically\n globeColor,\n size = 24,\n isAnimating = false,\n startOnHover = true,\n animationType = 'rotate',\n}: GlobalSearchIconProps) {\n const controls = useAnimation();\n const globeControls = useAnimation(); // For the inner globe\n const [isHovered, setIsHovered] = useState(false);\n const shouldAnimate = isAnimating || (startOnHover && isHovered);\n\n const finalGlobeColor = globeColor || color;\n\n useEffect(() => {\n if (shouldAnimate) {\n if (animationType === 'rotate') {\n globeControls.start({\n rotate: 360,\n transition: {\n duration: 4,\n repeat: Infinity,\n ease: 'linear',\n },\n });\n } else if (animationType === 'search') {\n controls.start({\n x: [0, 5, -5, 0, 5, -5, 0],\n y: [0, 5, 5, 0, -5, -5, 0],\n transition: {\n duration: 2,\n repeat: Infinity,\n ease: 'easeInOut',\n },\n });\n } else if (animationType === 'shake') {\n controls.start({\n x: [0, -3, 3, -3, 3, 0],\n transition: {\n duration: 0.5,\n repeat: Infinity,\n repeatDelay: 1,\n ease: 'easeInOut',\n },\n });\n }\n } else {\n controls.stop();\n controls.set({ x: 0, y: 0 });\n globeControls.stop();\n globeControls.set({ rotate: 0 });\n }\n }, [shouldAnimate, animationType, controls, globeControls]);\n\n return (\n setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n >\n \n {/* Magnifying Glass Handle */}\n \n\n {/* Magnifying Glass Ring */}\n \n\n {/* Globe Inside - Masked or just placed carefully inside */}\n \n \n \n \n \n \n \n \n );\n}\n", + "type": "registry:ui" + } + ] +} \ No newline at end of file diff --git a/public/e/heart-icon-basic.json b/public/e/heart-icon-basic.json new file mode 100644 index 0000000..38ce57c --- /dev/null +++ b/public/e/heart-icon-basic.json @@ -0,0 +1,18 @@ +{ + "name": "heart-icon-basic", + "type": "registry:ui", + "componentName": "heart-icon-basic", + "description": "Basic usage of the Heart Icon.", + "files": [ + { + "path": "heart-icon-basic.tsx", + "content": "'use client';\n\nimport { HeartIcon } from '@/components/core/heart-icon';\n\nexport default function HeartIconBasic() {\n return (\n
\n
\n \n \n \n
\n

\n Hover to see animations. Center shows beating heart with custom colors.\n

\n
\n );\n}\n", + "type": "registry:component" + }, + { + "path": "components/core/heart-icon.tsx", + "content": "'use client';\n\nimport { motion, useAnimation } from 'motion/react';\nimport { cn } from '@/lib/utils';\nimport { useEffect, useState } from 'react';\n\ninterface HeartIconProps {\n className?: string;\n color?: string; // Heart outline color\n shineColor?: string; // Shine/Reflection color\n size?: number;\n isAnimating?: boolean;\n startOnHover?: boolean;\n animationType?: 'bounce' | 'beat' | 'pulse';\n}\n\nexport function HeartIcon({\n className,\n color = 'currentColor', // Black typically\n shineColor,\n size = 24,\n isAnimating = false,\n startOnHover = true,\n animationType = 'bounce',\n}: HeartIconProps) {\n const controls = useAnimation();\n const [isHovered, setIsHovered] = useState(false);\n const shouldAnimate = isAnimating || (startOnHover && isHovered);\n\n const finalShineColor = shineColor || color;\n\n useEffect(() => {\n if (shouldAnimate) {\n if (animationType === 'bounce') {\n controls.start({\n y: -5,\n transition: {\n duration: 0.5,\n repeat: Infinity,\n repeatType: 'reverse',\n ease: 'easeInOut',\n },\n });\n } else if (animationType === 'beat') {\n // Classic heartbeat: thump-thump... thump-thump...\n controls.start({\n scale: [1, 1.2, 1, 1.2, 1],\n transition: {\n duration: 1, // 1 second for the double beat\n repeat: Infinity,\n repeatDelay: 0.5, // Pause between beats\n ease: 'easeInOut',\n },\n });\n } else if (animationType === 'pulse') {\n controls.start({\n scale: [1, 1.1, 1],\n transition: {\n duration: 1.5,\n repeat: Infinity,\n ease: 'easeInOut',\n },\n });\n }\n } else {\n controls.stop();\n controls.set({ y: 0, scale: 1 });\n }\n }, [shouldAnimate, animationType, controls]);\n\n return (\n setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n >\n \n \n {/* Heart Shape */}\n \n {/* Shine - Top Right Lobe */}\n \n \n \n \n );\n}\n", + "type": "registry:ui" + } + ] +} \ No newline at end of file diff --git a/public/e/like-icon-basic.json b/public/e/like-icon-basic.json new file mode 100644 index 0000000..9c8f6a4 --- /dev/null +++ b/public/e/like-icon-basic.json @@ -0,0 +1,18 @@ +{ + "name": "like-icon-basic", + "type": "registry:ui", + "componentName": "like-icon-basic", + "description": "Basic usage of the Like Icon.", + "files": [ + { + "path": "like-icon-basic.tsx", + "content": "'use client';\n\nimport { LikeIcon } from '@/components/core/like-icon';\n\nexport default function LikeIconBasic() {\n return (\n
\n
\n \n \n \n
\n

\n Hover to see animations. Center shows "like" reaction with\n custom colors.\n

\n
\n );\n}\n", + "type": "registry:component" + }, + { + "path": "components/core/like-icon.tsx", + "content": "'use client';\n\nimport { motion, useAnimation } from 'motion/react';\nimport { cn } from '@/lib/utils';\nimport { useEffect, useState } from 'react';\n\ninterface LikeIconProps {\n className?: string;\n color?: string; // Hand color\n cuffColor?: string; // Cuff/Sleeve color\n size?: number;\n isAnimating?: boolean;\n startOnHover?: boolean;\n animationType?: 'bounce' | 'like' | 'wiggle';\n}\n\nexport function LikeIcon({\n className,\n color = 'currentColor', // Black typically\n cuffColor,\n size = 24,\n isAnimating = false,\n startOnHover = true,\n animationType = 'bounce',\n}: LikeIconProps) {\n const controls = useAnimation();\n const thumbControls = useAnimation();\n const [isHovered, setIsHovered] = useState(false);\n const shouldAnimate = isAnimating || (startOnHover && isHovered);\n\n const finalCuffColor = cuffColor || color;\n\n useEffect(() => {\n if (shouldAnimate) {\n if (animationType === 'bounce') {\n controls.start({\n y: -5,\n transition: {\n duration: 0.5,\n repeat: Infinity,\n repeatType: 'reverse',\n ease: 'easeInOut',\n },\n });\n } else if (animationType === 'like') {\n controls.start({\n scale: [1, 1.2, 1],\n rotate: [0, -15, 0],\n transition: {\n duration: 0.6,\n repeat: Infinity,\n repeatDelay: 1,\n ease: 'easeInOut',\n },\n });\n } else if (animationType === 'wiggle') {\n // Wiggle the thumb part specifically if possible, or the whole hand\n thumbControls.start({\n rotate: [0, -15, 15, -10, 10, 0],\n originX: '6px',\n originY: '14px', // Approximate pivot for thumb\n transition: {\n duration: 1,\n repeat: Infinity,\n ease: 'easeInOut',\n },\n });\n }\n } else {\n controls.stop();\n controls.set({ y: 0, scale: 1, rotate: 0 });\n thumbControls.stop();\n thumbControls.set({ rotate: 0 });\n }\n }, [shouldAnimate, animationType, controls, thumbControls]);\n\n return (\n setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n >\n \n \n {/* Cuff / Sleeve */}\n \n\n {/* Hand Main Body (Fingers) */}\n \n\n {/* Thumb */}\n \n \n \n \n );\n}\n", + "type": "registry:ui" + } + ] +} \ No newline at end of file diff --git a/public/e/mail-stack-icon-basic.json b/public/e/mail-stack-icon-basic.json new file mode 100644 index 0000000..e893eb4 --- /dev/null +++ b/public/e/mail-stack-icon-basic.json @@ -0,0 +1,18 @@ +{ + "name": "mail-stack-icon-basic", + "type": "registry:ui", + "componentName": "mail-stack-icon-basic", + "description": "Basic usage of the Mail Stack Icon.", + "files": [ + { + "path": "mail-stack-icon-basic.tsx", + "content": "'use client';\n\nimport { MailStackIcon } from '@/components/core/mail-stack-icon';\n\nexport default function MailStackIconBasic() {\n return (\n
\n
\n \n \n \n
\n

\n Hover to see animations. Center shows sliding stack with custom colors.\n

\n
\n );\n}\n", + "type": "registry:component" + }, + { + "path": "components/core/mail-stack-icon.tsx", + "content": "'use client';\n\nimport { motion, useAnimation } from 'motion/react';\nimport { cn } from '@/lib/utils';\nimport { useEffect, useState } from 'react';\n\ninterface MailStackIconProps {\n className?: string;\n color?: string; // Front envelope color\n stackColor?: string; // Background stack color\n size?: number;\n isAnimating?: boolean;\n startOnHover?: boolean;\n animationType?: 'bounce' | 'slide' | 'rotate';\n}\n\nexport function MailStackIcon({\n className,\n color = 'currentColor', // Front envelope\n stackColor,\n size = 24,\n isAnimating = false,\n startOnHover = true,\n animationType = 'bounce',\n}: MailStackIconProps) {\n const controls = useAnimation();\n const stackControls = useAnimation();\n const [isHovered, setIsHovered] = useState(false);\n const shouldAnimate = isAnimating || (startOnHover && isHovered);\n\n const finalStackColor = stackColor || color;\n\n useEffect(() => {\n if (shouldAnimate) {\n if (animationType === 'bounce') {\n controls.start({\n y: -4,\n transition: {\n duration: 0.5,\n repeat: Infinity,\n repeatType: 'reverse',\n ease: 'easeInOut',\n },\n });\n stackControls.start({\n y: -4,\n transition: {\n duration: 0.5,\n repeat: Infinity,\n repeatType: 'reverse',\n ease: 'easeInOut',\n delay: 0.1, // Stagger effect\n },\n });\n } else if (animationType === 'slide') {\n stackControls.start({\n x: [0, 4, 0],\n y: [0, -4, 0],\n transition: {\n duration: 1.5,\n repeat: Infinity,\n ease: 'easeInOut',\n },\n });\n } else if (animationType === 'rotate') {\n stackControls.start({\n rotate: [0, -10, 0],\n transition: {\n duration: 2,\n repeat: Infinity,\n ease: 'easeInOut',\n originX: '12px',\n originY: '12px',\n },\n });\n controls.start({\n rotate: [0, 5, 0],\n transition: {\n duration: 2,\n repeat: Infinity,\n ease: 'easeInOut',\n originX: '12px',\n originY: '12px',\n },\n });\n }\n } else {\n controls.stop();\n controls.set({ x: 0, y: 0, rotate: 0 });\n stackControls.stop();\n stackControls.set({ x: 0, y: 0, rotate: 0 });\n }\n }, [shouldAnimate, animationType, controls, stackControls]);\n\n return (\n setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n >\n \n {/* Background Stack 2 (Furthest) */}\n \n\n {/* Background Stack 1 (Middle) */}\n \n\n {/* Front Envelope */}\n \n \n \n \n \n \n );\n}\n", + "type": "registry:ui" + } + ] +} \ No newline at end of file diff --git a/public/e/microphone-icon-basic.json b/public/e/microphone-icon-basic.json new file mode 100644 index 0000000..655f966 --- /dev/null +++ b/public/e/microphone-icon-basic.json @@ -0,0 +1,18 @@ +{ + "name": "microphone-icon-basic", + "type": "registry:ui", + "componentName": "microphone-icon-basic", + "description": "Basic usage of the Microphone Icon.", + "files": [ + { + "path": "microphone-icon-basic.tsx", + "content": "'use client';\n\nimport { MicrophoneIcon } from '@/components/core/microphone-icon';\n\nexport default function MicrophoneIconBasic() {\n return (\n
\n
\n \n \n \n
\n

\n Hover to see animations. Center shows shaking mic with custom colors.\n

\n
\n );\n}\n", + "type": "registry:component" + }, + { + "path": "components/core/microphone-icon.tsx", + "content": "'use client';\n\nimport { motion, useAnimation } from 'motion/react';\nimport { cn } from '@/lib/utils';\nimport { useEffect, useState } from 'react';\n\ninterface MicrophoneIconProps {\n className?: string;\n color?: string; // Mic body color\n standColor?: string; // Stand/Base color\n size?: number;\n isAnimating?: boolean;\n startOnHover?: boolean;\n animationType?: 'bounce' | 'shake' | 'pulse';\n}\n\nexport function MicrophoneIcon({\n className,\n color = 'currentColor', // Mic body\n standColor,\n size = 24,\n isAnimating = false,\n startOnHover = true,\n animationType = 'bounce',\n}: MicrophoneIconProps) {\n const controls = useAnimation();\n const standControls = useAnimation();\n const [isHovered, setIsHovered] = useState(false);\n const shouldAnimate = isAnimating || (startOnHover && isHovered);\n\n const finalStandColor = standColor || color;\n\n useEffect(() => {\n if (shouldAnimate) {\n if (animationType === 'bounce') {\n controls.start({\n y: -4,\n transition: {\n duration: 0.5,\n repeat: Infinity,\n repeatType: 'reverse',\n ease: 'easeInOut',\n },\n });\n standControls.start({\n y: -4,\n transition: {\n duration: 0.5,\n repeat: Infinity,\n repeatType: 'reverse',\n ease: 'easeInOut',\n },\n });\n } else if (animationType === 'shake') {\n controls.start({\n x: [-2, 2, -2, 2, 0],\n transition: {\n duration: 0.4,\n repeat: Infinity,\n repeatDelay: 1, // Periodic shake like voice input\n ease: 'easeInOut',\n },\n });\n } else if (animationType === 'pulse') {\n controls.start({\n scale: [1, 1.1, 1],\n opacity: [1, 0.8, 1],\n transition: {\n duration: 1.5,\n repeat: Infinity,\n ease: 'easeInOut',\n },\n });\n }\n } else {\n controls.stop();\n controls.set({ y: 0, x: 0, scale: 1, opacity: 1 });\n standControls.stop();\n standControls.set({ y: 0 });\n }\n }, [shouldAnimate, animationType, controls, standControls]);\n\n return (\n setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n >\n \n {/* Mic Body */}\n \n \n \n \n \n \n\n {/* Mic Stand */}\n \n {/* U-Shape Holder */}\n \n {/* Stem */}\n \n {/* Base */}\n \n \n \n \n );\n}\n", + "type": "registry:ui" + } + ] +} \ No newline at end of file diff --git a/public/e/network-icon-basic.json b/public/e/network-icon-basic.json index 4c5479e..76f3544 100644 --- a/public/e/network-icon-basic.json +++ b/public/e/network-icon-basic.json @@ -11,7 +11,7 @@ }, { "path": "components/core/network-icon.tsx", - "content": "'use client';\n\nimport { motion, useAnimation } from 'motion/react';\nimport { User } from 'lucide-react';\nimport { cn } from '@/lib/utils';\nimport { useEffect, useState } from 'react';\n\ninterface NetworkIconProps {\n className?: string;\n color?: string;\n size?: number;\n isAnimating?: boolean;\n startOnHover?: boolean;\n animationType?: 'pulse' | 'expand';\n}\n\nexport function NetworkIcon({\n className,\n color = 'currentColor',\n size = 24,\n isAnimating = false,\n startOnHover = true,\n animationType = 'expand',\n}: NetworkIconProps) {\n const controls = useAnimation();\n const [isHovered, setIsHovered] = useState(false);\n const shouldAnimate = isAnimating || (startOnHover && isHovered);\n\n // Calculate sizes relative to the main size\n const userSize = size * 0.5;\n const nodeSize = size * 0.15;\n const radius = size * 0.35; // Distance from center to nodes\n\n useEffect(() => {\n if (shouldAnimate) {\n if (animationType === 'expand') {\n controls.start({\n scale: [1, 1.1, 1],\n opacity: [1, 0.8, 1],\n transition: {\n duration: 1.5,\n repeat: Infinity,\n repeatType: 'loop',\n ease: 'easeInOut',\n },\n });\n } else if (animationType === 'pulse') {\n controls.start({\n opacity: [0.5, 1, 0.5],\n scale: [1, 1.05, 1],\n transition: {\n duration: 2,\n repeat: Infinity,\n repeatType: 'loop',\n ease: 'easeInOut',\n },\n });\n }\n } else {\n controls.stop();\n controls.set({ scale: 1, opacity: 1 });\n }\n }, [shouldAnimate, animationType, controls]);\n\n // Node positions (normalized 0-1, centered at 0.5)\n // Top Left, Top Right, Bottom Left, Bottom Right\n const nodes = [\n { x: 0.2, y: 0.2 },\n { x: 0.8, y: 0.2 },\n { x: 0.2, y: 0.8 },\n { x: 0.8, y: 0.8 },\n ];\n\n return (\n setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n >\n {/* Central User Icon */}\n
\n \n
\n\n {/* Connecting Lines and Nodes */}\n \n {nodes.map((node, i) => {\n // Center of canvas\n const cx = size / 2;\n const cy = size / 2;\n\n // Node position\n const nx = node.x * size;\n const ny = node.y * size;\n\n return (\n \n {/* Line from center to node */}\n \n {/* Node circle */}\n \n \n );\n })}\n \n \n );\n}\n", + "content": "'use client';\n\nimport { motion, useAnimation } from 'motion/react';\nimport { User } from 'lucide-react';\nimport { cn } from '@/lib/utils';\nimport { useEffect, useState } from 'react';\n\ninterface NetworkIconProps {\n className?: string;\n color?: string;\n size?: number;\n isAnimating?: boolean;\n startOnHover?: boolean;\n animationType?: 'pulse' | 'expand';\n}\n\nexport function NetworkIcon({\n className,\n color = 'currentColor',\n size = 24,\n isAnimating = false,\n startOnHover = true,\n animationType = 'expand',\n}: NetworkIconProps) {\n const controls = useAnimation();\n const [isHovered, setIsHovered] = useState(false);\n const shouldAnimate = isAnimating || (startOnHover && isHovered);\n\n // Calculate sizes relative to the main size\n const userSize = size * 0.5;\n const nodeSize = size * 0.15;\n\n useEffect(() => {\n if (shouldAnimate) {\n if (animationType === 'expand') {\n controls.start({\n scale: [1, 1.1, 1],\n opacity: [1, 0.8, 1],\n transition: {\n duration: 1.5,\n repeat: Infinity,\n repeatType: 'loop',\n ease: 'easeInOut',\n },\n });\n } else if (animationType === 'pulse') {\n controls.start({\n opacity: [0.5, 1, 0.5],\n scale: [1, 1.05, 1],\n transition: {\n duration: 2,\n repeat: Infinity,\n repeatType: 'loop',\n ease: 'easeInOut',\n },\n });\n }\n } else {\n controls.stop();\n controls.set({ scale: 1, opacity: 1 });\n }\n }, [shouldAnimate, animationType, controls]);\n\n // Node positions (normalized 0-1, centered at 0.5)\n // Top Left, Top Right, Bottom Left, Bottom Right\n const nodes = [\n { x: 0.2, y: 0.2 },\n { x: 0.8, y: 0.2 },\n { x: 0.2, y: 0.8 },\n { x: 0.8, y: 0.8 },\n ];\n\n return (\n setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n >\n {/* Central User Icon */}\n
\n \n
\n\n {/* Connecting Lines and Nodes */}\n \n {nodes.map((node, i) => {\n // Center of canvas\n const cx = size / 2;\n const cy = size / 2;\n\n // Node position\n const nx = node.x * size;\n const ny = node.y * size;\n\n return (\n \n {/* Line from center to node */}\n \n {/* Node circle */}\n \n \n );\n })}\n \n \n );\n}\n", "type": "registry:ui" } ] diff --git a/public/e/paper-plane-icon-basic.json b/public/e/paper-plane-icon-basic.json new file mode 100644 index 0000000..f4c8ef1 --- /dev/null +++ b/public/e/paper-plane-icon-basic.json @@ -0,0 +1,18 @@ +{ + "name": "paper-plane-icon-basic", + "type": "registry:ui", + "componentName": "paper-plane-icon-basic", + "description": "Basic usage of the Paper Plane Icon.", + "files": [ + { + "path": "paper-plane-icon-basic.tsx", + "content": "'use client';\n\nimport { PaperPlaneIcon } from '@/components/core/paper-plane-icon';\n\nexport default function PaperPlaneIconBasic() {\n return (\n
\n
\n \n \n \n
\n

\n Hover to see animations. Center shows flying plane with custom colors.\n

\n
\n );\n}\n", + "type": "registry:component" + }, + { + "path": "components/core/paper-plane-icon.tsx", + "content": "'use client';\n\nimport { motion, useAnimation } from 'motion/react';\nimport { cn } from '@/lib/utils';\nimport { useEffect, useState } from 'react';\n\ninterface PaperPlaneIconProps {\n className?: string;\n color?: string; // Plane color\n trailColor?: string; // Trail color\n size?: number;\n isAnimating?: boolean;\n startOnHover?: boolean;\n animationType?: 'bounce' | 'fly' | 'wobble';\n}\n\nexport function PaperPlaneIcon({\n className,\n color = 'currentColor', // Plane\n trailColor,\n size = 24,\n isAnimating = false,\n startOnHover = true,\n animationType = 'bounce',\n}: PaperPlaneIconProps) {\n const controls = useAnimation();\n const trailControls = useAnimation();\n const [isHovered, setIsHovered] = useState(false);\n const shouldAnimate = isAnimating || (startOnHover && isHovered);\n\n const finalTrailColor = trailColor || color;\n\n useEffect(() => {\n if (shouldAnimate) {\n if (animationType === 'bounce') {\n controls.start({\n y: -5,\n transition: {\n duration: 0.5,\n repeat: Infinity,\n repeatType: 'reverse',\n ease: 'easeInOut',\n },\n });\n trailControls.start({\n opacity: [0, 1, 0],\n transition: {\n duration: 1,\n repeat: Infinity,\n ease: 'easeInOut',\n },\n });\n } else if (animationType === 'fly') {\n controls.start({\n x: [0, 20],\n y: [0, -20],\n opacity: [1, 0],\n transition: {\n duration: 1,\n repeat: Infinity,\n ease: 'easeIn',\n },\n });\n trailControls.start({\n opacity: [1, 0],\n pathLength: [0, 1],\n transition: {\n duration: 1,\n repeat: Infinity,\n ease: 'easeIn',\n },\n });\n } else if (animationType === 'wobble') {\n controls.start({\n rotate: [0, -10, 10, -5, 5, 0],\n transition: {\n duration: 2,\n repeat: Infinity,\n ease: 'easeInOut',\n originX: '12px',\n originY: '12px',\n },\n });\n }\n } else {\n controls.stop();\n controls.set({ x: 0, y: 0, opacity: 1, rotate: 0 });\n trailControls.stop();\n trailControls.set({ opacity: 1, pathLength: 1 });\n }\n }, [shouldAnimate, animationType, controls, trailControls]);\n\n return (\n setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n >\n \n {/* Trail - Dashed line behind */}\n \n \n\n {/* Paper Plane */}\n \n {/* Main Body */}\n \n \n \n \n \n );\n}\n", + "type": "registry:ui" + } + ] +} \ No newline at end of file diff --git a/public/e/phone-icon-basic.json b/public/e/phone-icon-basic.json index 482c339..8ab6182 100644 --- a/public/e/phone-icon-basic.json +++ b/public/e/phone-icon-basic.json @@ -6,7 +6,7 @@ "files": [ { "path": "phone-icon-basic.tsx", - "content": "'use client';\n\nimport { PhoneIcon } from '@/components/core/phone-icon';\nimport { useState } from 'react';\n\nexport default function PhoneIconBasic() {\n return (\n
\n \n

Hover to ring

\n
\n );\n}\n", + "content": "'use client';\n\nimport { PhoneIcon } from '@/components/core/phone-icon';\n\nexport default function PhoneIconBasic() {\n return (\n
\n \n

Hover to ring

\n
\n );\n}\n", "type": "registry:component" }, { diff --git a/public/e/share-icon-basic.json b/public/e/share-icon-basic.json new file mode 100644 index 0000000..2d45149 --- /dev/null +++ b/public/e/share-icon-basic.json @@ -0,0 +1,18 @@ +{ + "name": "share-icon-basic", + "type": "registry:ui", + "componentName": "share-icon-basic", + "description": "Basic usage of the Share Icon.", + "files": [ + { + "path": "share-icon-basic.tsx", + "content": "'use client';\n\nimport { ShareIcon } from '@/components/core/share-icon';\n\nexport default function ShareIconBasic() {\n return (\n
\n
\n \n \n \n
\n

\n Hover to see animations. Center shows rotating graph with custom colors.\n

\n
\n );\n}\n", + "type": "registry:component" + }, + { + "path": "components/core/share-icon.tsx", + "content": "'use client';\n\nimport { motion, useAnimation } from 'motion/react';\nimport { cn } from '@/lib/utils';\nimport { useEffect, useState } from 'react';\n\ninterface ShareIconProps {\n className?: string;\n color?: string; // Lines/Connection color\n dotColor?: string; // Dots color\n size?: number;\n isAnimating?: boolean;\n startOnHover?: boolean;\n animationType?: 'bounce' | 'pulse' | 'rotate';\n}\n\nexport function ShareIcon({\n className,\n color = 'currentColor', // Lines\n dotColor,\n size = 24,\n isAnimating = false,\n startOnHover = true,\n animationType = 'bounce',\n}: ShareIconProps) {\n const controls = useAnimation();\n const dotControls = useAnimation();\n const [isHovered, setIsHovered] = useState(false);\n const shouldAnimate = isAnimating || (startOnHover && isHovered);\n\n const finalDotColor = dotColor || color;\n\n useEffect(() => {\n if (shouldAnimate) {\n if (animationType === 'bounce') {\n controls.start({\n y: -5,\n transition: {\n duration: 0.5,\n repeat: Infinity,\n repeatType: 'reverse',\n ease: 'easeInOut',\n },\n });\n } else if (animationType === 'pulse') {\n dotControls.start({\n scale: [1, 1.3, 1],\n transition: {\n duration: 1,\n repeat: Infinity,\n ease: 'easeInOut',\n },\n });\n } else if (animationType === 'rotate') {\n // Rotate the whole group or just the branches?\n // Let's rotate the whole icon for \"share\" loading effect, or maybe just the right dots around the left one.\n // Let's rotate connected dots if possible.\n // Actually, simple rotation of the whole icon around the center is easiest and looks \"busy\".\n // But pivoting around the left dot (cx=6, cy=12) is cooler.\n controls.start({\n rotate: 360,\n transition: {\n duration: 2,\n repeat: Infinity,\n ease: 'linear',\n originX: '6px',\n originY: '12px', // Pivot around the left dot\n },\n });\n }\n } else {\n controls.stop();\n controls.set({ y: 0, rotate: 0 });\n dotControls.stop();\n dotControls.set({ scale: 1 });\n }\n }, [shouldAnimate, animationType, controls, dotControls]);\n\n return (\n setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n >\n \n \n {/* Connections */}\n {' '}\n {/* Anchor point helper if needed */}\n \n \n \n \n \n {/* Re-drawing circles on top if we want dots to pulse separately */}\n \n \n \n \n \n \n );\n}\n", + "type": "registry:ui" + } + ] +} \ No newline at end of file diff --git a/public/e/tools-icon-basic.json b/public/e/tools-icon-basic.json new file mode 100644 index 0000000..78276e0 --- /dev/null +++ b/public/e/tools-icon-basic.json @@ -0,0 +1,18 @@ +{ + "name": "tools-icon-basic", + "type": "registry:ui", + "componentName": "tools-icon-basic", + "description": "Basic usage of the Tools Icon.", + "files": [ + { + "path": "tools-icon-basic.tsx", + "content": "'use client';\n\nimport { ToolsIcon } from '@/components/core/tools-icon';\n\nexport default function ToolsIconBasic() {\n return (\n
\n
\n \n \n \n
\n

\n Hover to see animations. Center shows "repair" with custom\n colors.\n

\n
\n );\n}\n", + "type": "registry:component" + }, + { + "path": "components/core/tools-icon.tsx", + "content": "'use client';\n\nimport { motion, useAnimation } from 'motion/react';\nimport { cn } from '@/lib/utils';\nimport { useEffect, useState } from 'react';\n\ninterface ToolsIconProps {\n className?: string;\n color?: string; // Wrench color (Primary)\n screwdriverColor?: string; // Screwdriver color (Secondary)\n size?: number;\n isAnimating?: boolean;\n startOnHover?: boolean;\n animationType?: 'bounce' | 'wiggle' | 'repair';\n}\n\nexport function ToolsIcon({\n className,\n color = 'currentColor', // Wrench\n screwdriverColor,\n size = 24,\n isAnimating = false,\n startOnHover = true,\n animationType = 'bounce',\n}: ToolsIconProps) {\n const wrenchControls = useAnimation();\n const screwdriverControls = useAnimation();\n const [isHovered, setIsHovered] = useState(false);\n const shouldAnimate = isAnimating || (startOnHover && isHovered);\n\n const finalScrewdriverColor = screwdriverColor || color;\n\n useEffect(() => {\n if (shouldAnimate) {\n if (animationType === 'bounce') {\n wrenchControls.start({\n y: -4,\n transition: {\n duration: 0.5,\n repeat: Infinity,\n repeatType: 'reverse',\n ease: 'easeInOut',\n },\n });\n screwdriverControls.start({\n y: -4,\n transition: {\n duration: 0.5,\n repeat: Infinity,\n repeatType: 'reverse',\n ease: 'easeInOut',\n delay: 0.1,\n },\n });\n } else if (animationType === 'wiggle') {\n wrenchControls.start({\n rotate: [0, -10, 10, -5, 5, 0],\n transition: { duration: 1.5, repeat: Infinity, ease: 'linear' },\n });\n screwdriverControls.start({\n rotate: [0, 10, -10, 5, -5, 0],\n transition: {\n duration: 1.5,\n repeat: Infinity,\n ease: 'linear',\n delay: 0.1,\n },\n });\n } else if (animationType === 'repair') {\n // Pivot movement\n wrenchControls.start({\n rotate: [0, -20, 0, -10, 0],\n transition: { duration: 2, repeat: Infinity, ease: 'easeInOut' },\n });\n screwdriverControls.start({\n rotate: [0, 20, 0, 10, 0],\n transition: {\n duration: 2,\n repeat: Infinity,\n ease: 'easeInOut',\n delay: 0.2,\n },\n });\n }\n } else {\n wrenchControls.stop();\n wrenchControls.set({ y: 0, rotate: 0 });\n screwdriverControls.stop();\n screwdriverControls.set({ y: 0, rotate: 0 });\n }\n }, [shouldAnimate, animationType, wrenchControls, screwdriverControls]);\n\n return (\n setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n >\n \n {/* Screwdriver: Handle Bottom Left, Tip Top Right */}\n \n {/* Handle */}\n \n \n {/* Shaft */}\n \n {/* Tip */}\n \n \n\n {/* Wrench: Handle Bottom Right, Head Top Left */}\n \n {/* Handle */}\n \n {/* Head */}\n \n \n \n \n\n {/* Re-doing paths to be simpler and cleaner standard crossed tools */}\n {/* Using known good path data logic */}\n\n \n {/* Screwdriver */}\n \n {/* Simple lines for screwdriver is tricky, let's use the Lucide one but oriented */}\n \n \n \n \n \n\n \n {/* Wrench */}\n \n \n \n \n {/* FINAL CLEAN VERSION: Overwriting the whole SVG content with verified paths */}\n \n {/* Screwdriver (Back) */}\n \n \n \n \n \n \n\n {/* Wrench (Front) */}\n \n \n \n \n \n \n );\n}\n", + "type": "registry:ui" + } + ] +} \ No newline at end of file diff --git a/public/e/trash-icon-basic.json b/public/e/trash-icon-basic.json new file mode 100644 index 0000000..34c9d56 --- /dev/null +++ b/public/e/trash-icon-basic.json @@ -0,0 +1,18 @@ +{ + "name": "trash-icon-basic", + "type": "registry:ui", + "componentName": "trash-icon-basic", + "description": "Basic usage of the Trash Icon.", + "files": [ + { + "path": "trash-icon-basic.tsx", + "content": "'use client';\n\nimport { TrashIcon } from '@/components/core/trash-icon';\n\nexport default function TrashIconBasic() {\n return (\n
\n
\n \n \n \n
\n

\n Hover to see animations. Center shows lid flipping with custom colors.\n

\n
\n );\n}\n", + "type": "registry:component" + }, + { + "path": "components/core/trash-icon.tsx", + "content": "'use client';\n\nimport { motion, useAnimation } from 'motion/react';\nimport { cn } from '@/lib/utils';\nimport { useEffect, useState } from 'react';\n\ninterface TrashIconProps {\n className?: string;\n color?: string; // Bin color\n lidColor?: string; // Lid color\n size?: number;\n isAnimating?: boolean;\n startOnHover?: boolean;\n animationType?: 'bounce' | 'trash' | 'shake';\n}\n\nexport function TrashIcon({\n className,\n color = 'currentColor', // Black typically\n lidColor,\n size = 24,\n isAnimating = false,\n startOnHover = true,\n animationType = 'bounce',\n}: TrashIconProps) {\n const controls = useAnimation();\n const lidControls = useAnimation();\n const [isHovered, setIsHovered] = useState(false);\n const shouldAnimate = isAnimating || (startOnHover && isHovered);\n\n const finalLidColor = lidColor || color;\n\n useEffect(() => {\n if (shouldAnimate) {\n if (animationType === 'bounce') {\n controls.start({\n y: -5,\n transition: {\n duration: 0.5,\n repeat: Infinity,\n repeatType: 'reverse',\n ease: 'easeInOut',\n },\n });\n lidControls.start({\n y: -5,\n transition: {\n duration: 0.5,\n repeat: Infinity,\n repeatType: 'reverse',\n ease: 'easeInOut',\n },\n });\n } else if (animationType === 'trash') {\n // Lid flip animation\n lidControls.start({\n rotate: [0, -45, 0],\n originX: '2px',\n originY: '5px', // Pivot at left/bottom of lid\n transition: {\n duration: 0.8,\n repeat: Infinity,\n repeatDelay: 0.5,\n ease: 'easeInOut',\n },\n });\n } else if (animationType === 'shake') {\n controls.start({\n x: [-2, 2, -2, 2, 0],\n transition: {\n duration: 0.5,\n repeat: Infinity,\n repeatDelay: 1,\n ease: 'easeInOut',\n },\n });\n lidControls.start({\n x: [-2, 2, -2, 2, 0],\n transition: {\n duration: 0.5,\n repeat: Infinity,\n repeatDelay: 1,\n ease: 'easeInOut',\n },\n });\n }\n } else {\n controls.stop();\n controls.set({ y: 0, x: 0 });\n lidControls.stop();\n lidControls.set({ y: 0, x: 0, rotate: 0 });\n }\n }, [shouldAnimate, animationType, controls, lidControls]);\n\n return (\n setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n >\n \n {/* Bin Body */}\n \n {' '}\n {/* Spacer/invisible line if needed, or just rely on following paths */}\n \n \n \n \n\n {/* Lid Group */}\n \n \n \n {/* We duplicate the top line of bin here essentially to make it part of the lid if we want it to move with it, \n or we keep the line separated. Usually 'Trash' icon has a solid line across top. \n Let's make the line x1=3 x2=21 part of the lid animation for 'trash' flip effect.\n */}\n \n \n \n );\n}\n", + "type": "registry:ui" + } + ] +} \ No newline at end of file diff --git a/public/e/trophy-icon-basic.json b/public/e/trophy-icon-basic.json new file mode 100644 index 0000000..4c2bf72 --- /dev/null +++ b/public/e/trophy-icon-basic.json @@ -0,0 +1,18 @@ +{ + "name": "trophy-icon-basic", + "type": "registry:ui", + "componentName": "trophy-icon-basic", + "description": "Basic usage of the Trophy Icon.", + "files": [ + { + "path": "trophy-icon-basic.tsx", + "content": "'use client';\n\nimport { TrophyIcon } from '@/components/core/trophy-icon';\n\nexport default function TrophyIconBasic() {\n return (\n
\n
\n \n \n \n
\n

\n Hover to see animations. Center shows pulsing star with custom colors.\n

\n
\n );\n}\n", + "type": "registry:component" + }, + { + "path": "components/core/trophy-icon.tsx", + "content": "'use client';\n\nimport { motion, useAnimation } from 'motion/react';\nimport { cn } from '@/lib/utils';\nimport { useEffect, useState } from 'react';\n\ninterface TrophyIconProps {\n className?: string;\n color?: string; // Trophy color (Cup + Handles + Base)\n starColor?: string; // Star color\n size?: number;\n isAnimating?: boolean;\n startOnHover?: boolean;\n animationType?: 'bounce' | 'wobble' | 'shine';\n}\n\nexport function TrophyIcon({\n className,\n color = 'currentColor', // Black frame typically\n starColor,\n size = 24,\n isAnimating = false,\n startOnHover = true,\n animationType = 'bounce',\n}: TrophyIconProps) {\n const controls = useAnimation();\n const starControls = useAnimation();\n const [isHovered, setIsHovered] = useState(false);\n const shouldAnimate = isAnimating || (startOnHover && isHovered);\n\n const finalStarColor = starColor || color;\n\n useEffect(() => {\n if (shouldAnimate) {\n if (animationType === 'bounce') {\n controls.start({\n y: -5,\n transition: {\n duration: 0.5,\n repeat: Infinity,\n repeatType: 'reverse',\n ease: 'easeInOut',\n },\n });\n } else if (animationType === 'wobble') {\n controls.start({\n rotate: [0, -10, 10, -5, 5, 0],\n transition: {\n duration: 1.5,\n repeat: Infinity,\n ease: 'easeInOut',\n },\n });\n } else if (animationType === 'shine') {\n starControls.start({\n scale: [1, 1.3, 1],\n opacity: [1, 0.7, 1],\n rotate: [0, 15, -15, 0],\n transition: {\n duration: 1.5,\n repeat: Infinity,\n ease: 'easeInOut',\n },\n });\n }\n } else {\n controls.stop();\n controls.set({ y: 0, rotate: 0 });\n starControls.stop();\n starControls.set({ scale: 1, opacity: 1, rotate: 0 });\n }\n }, [shouldAnimate, animationType, controls, starControls]);\n\n return (\n setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n >\n \n \n {/* Cup */}\n \n {/* Handles */}\n \n \n {/* Base Stem */}\n \n {/* Base */}\n \n \n\n {/* Star in Center */}\n \n \n \n \n \n );\n}\n", + "type": "registry:ui" + } + ] +} \ No newline at end of file diff --git a/public/e/user-icon-basic.json b/public/e/user-icon-basic.json new file mode 100644 index 0000000..5357c3c --- /dev/null +++ b/public/e/user-icon-basic.json @@ -0,0 +1,18 @@ +{ + "name": "user-icon-basic", + "type": "registry:ui", + "componentName": "user-icon-basic", + "description": "Basic usage of the User Icon.", + "files": [ + { + "path": "user-icon-basic.tsx", + "content": "'use client';\n\nimport { UserIcon } from '@/components/core/user-icon';\n\nexport default function UserIconBasic() {\n return (\n
\n
\n \n \n \n
\n

\n Hover to see animations. Center shows "hi" wave with custom\n colors.\n

\n
\n );\n}\n", + "type": "registry:component" + }, + { + "path": "components/core/user-icon.tsx", + "content": "'use client';\n\nimport { motion, useAnimation } from 'motion/react';\nimport { cn } from '@/lib/utils';\nimport { useEffect, useState } from 'react';\n\ninterface UserIconProps {\n className?: string;\n color?: string; // Frame color\n userColor?: string; // User silhouette color\n size?: number;\n isAnimating?: boolean;\n startOnHover?: boolean;\n animationType?: 'bounce' | 'hi' | 'pulse';\n}\n\nexport function UserIcon({\n className,\n color = 'currentColor', // Black frame typically\n userColor,\n size = 24,\n isAnimating = false,\n startOnHover = true,\n animationType = 'bounce',\n}: UserIconProps) {\n const controls = useAnimation();\n const userControls = useAnimation(); // For the inner user shape\n const [isHovered, setIsHovered] = useState(false);\n const shouldAnimate = isAnimating || (startOnHover && isHovered);\n\n const finalUserColor = userColor || color;\n\n useEffect(() => {\n if (shouldAnimate) {\n if (animationType === 'bounce') {\n controls.start({\n y: -4,\n transition: {\n duration: 0.5,\n repeat: Infinity,\n repeatType: 'reverse',\n ease: 'easeInOut',\n },\n });\n } else if (animationType === 'hi') {\n // A generic \"wave\" or \"tilt\" animation for the user silhouette\n userControls.start({\n rotate: [0, -10, 10, -5, 5, 0],\n transition: {\n duration: 1.5,\n repeat: Infinity,\n repeatDelay: 1, // Pause between waves\n ease: 'easeInOut',\n },\n });\n } else if (animationType === 'pulse') {\n userControls.start({\n scale: [1, 1.1, 1],\n opacity: [1, 0.8, 1],\n transition: {\n duration: 2,\n repeat: Infinity,\n ease: 'easeInOut',\n },\n });\n }\n } else {\n controls.stop();\n controls.set({ y: 0 });\n userControls.stop();\n userControls.set({ rotate: 0, scale: 1, opacity: 1 });\n }\n }, [shouldAnimate, animationType, controls, userControls]);\n\n return (\n setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n >\n \n {/* Frame: Outer Circle */}\n \n\n {/* User Silhouette: Head + Body */}\n \n {/* Head */}\n \n {/* Body (Shoulders/Torso) */}\n \n \n \n \n );\n}\n", + "type": "registry:ui" + } + ] +} \ No newline at end of file diff --git a/scripts/registry-examples.ts b/scripts/registry-examples.ts index fac6db3..eb2a5a6 100644 --- a/scripts/registry-examples.ts +++ b/scripts/registry-examples.ts @@ -774,4 +774,176 @@ export const examples: ExampleDefinition[] = [ }, ], }, + { + name: 'global-icon-basic', + path: path.join(__dirname, '../app/docs/icons/global-icon-basic.tsx'), + description: 'Basic usage of the Global Icon.', + componentName: 'global-icon-basic', + files: [ + { + name: 'global-icon.tsx', + path: path.join(__dirname, '../components/core/global-icon.tsx'), + type: 'registry:ui', + }, + ], + }, + { + name: 'user-icon-basic', + path: path.join(__dirname, '../app/docs/icons/user-icon-basic.tsx'), + description: 'Basic usage of the User Icon.', + componentName: 'user-icon-basic', + files: [ + { + name: 'user-icon.tsx', + path: path.join(__dirname, '../components/core/user-icon.tsx'), + type: 'registry:ui', + }, + ], + }, + { + name: 'global-search-icon-basic', + path: path.join( + __dirname, + '../app/docs/icons/global-search-icon-basic.tsx' + ), + description: 'Basic usage of the Global Search Icon.', + componentName: 'global-search-icon-basic', + files: [ + { + name: 'global-search-icon.tsx', + path: path.join(__dirname, '../components/core/global-search-icon.tsx'), + type: 'registry:ui', + }, + ], + }, + { + name: 'tools-icon-basic', + path: path.join(__dirname, '../app/docs/icons/tools-icon-basic.tsx'), + description: 'Basic usage of the Tools Icon.', + componentName: 'tools-icon-basic', + files: [ + { + name: 'tools-icon.tsx', + path: path.join(__dirname, '../components/core/tools-icon.tsx'), + type: 'registry:ui', + }, + ], + }, + { + name: 'trophy-icon-basic', + path: path.join(__dirname, '../app/docs/icons/trophy-icon-basic.tsx'), + description: 'Basic usage of the Trophy Icon.', + componentName: 'trophy-icon-basic', + files: [ + { + name: 'trophy-icon.tsx', + path: path.join(__dirname, '../components/core/trophy-icon.tsx'), + type: 'registry:ui', + }, + ], + }, + { + name: 'microphone-icon-basic', + path: path.join(__dirname, '../app/docs/icons/microphone-icon-basic.tsx'), + description: 'Basic usage of the Microphone Icon.', + componentName: 'microphone-icon-basic', + files: [ + { + name: 'microphone-icon.tsx', + path: path.join(__dirname, '../components/core/microphone-icon.tsx'), + type: 'registry:ui', + }, + ], + }, + { + name: 'like-icon-basic', + path: path.join(__dirname, '../app/docs/icons/like-icon-basic.tsx'), + description: 'Basic usage of the Like Icon.', + componentName: 'like-icon-basic', + files: [ + { + name: 'like-icon.tsx', + path: path.join(__dirname, '../components/core/like-icon.tsx'), + type: 'registry:ui', + }, + ], + }, + { + name: 'bell-icon-basic', + path: path.join(__dirname, '../app/docs/icons/bell-icon-basic.tsx'), + description: 'Basic usage of the Bell Icon.', + componentName: 'bell-icon-basic', + files: [ + { + name: 'bell-icon.tsx', + path: path.join(__dirname, '../components/core/bell-icon.tsx'), + type: 'registry:ui', + }, + ], + }, + { + name: 'heart-icon-basic', + path: path.join(__dirname, '../app/docs/icons/heart-icon-basic.tsx'), + description: 'Basic usage of the Heart Icon.', + componentName: 'heart-icon-basic', + files: [ + { + name: 'heart-icon.tsx', + path: path.join(__dirname, '../components/core/heart-icon.tsx'), + type: 'registry:ui', + }, + ], + }, + { + name: 'trash-icon-basic', + path: path.join(__dirname, '../app/docs/icons/trash-icon-basic.tsx'), + description: 'Basic usage of the Trash Icon.', + componentName: 'trash-icon-basic', + files: [ + { + name: 'trash-icon.tsx', + path: path.join(__dirname, '../components/core/trash-icon.tsx'), + type: 'registry:ui', + }, + ], + }, + { + name: 'share-icon-basic', + path: path.join(__dirname, '../app/docs/icons/share-icon-basic.tsx'), + description: 'Basic usage of the Share Icon.', + componentName: 'share-icon-basic', + files: [ + { + name: 'share-icon.tsx', + path: path.join(__dirname, '../components/core/share-icon.tsx'), + type: 'registry:ui', + }, + ], + }, + { + name: 'paper-plane-icon-basic', + path: path.join(__dirname, '../app/docs/icons/paper-plane-icon-basic.tsx'), + description: 'Basic usage of the Paper Plane Icon.', + componentName: 'paper-plane-icon-basic', + files: [ + { + name: 'paper-plane-icon.tsx', + path: path.join(__dirname, '../components/core/paper-plane-icon.tsx'), + type: 'registry:ui', + }, + ], + }, + { + name: 'mail-stack-icon-basic', + path: path.join(__dirname, '../app/docs/icons/mail-stack-icon-basic.tsx'), + description: 'Basic usage of the Mail Stack Icon.', + componentName: 'mail-stack-icon-basic', + files: [ + { + name: 'mail-stack-icon.tsx', + path: path.join(__dirname, '../components/core/mail-stack-icon.tsx'), + type: 'registry:ui', + }, + ], + }, ]; diff --git a/stories/bell-icon.stories.tsx b/stories/bell-icon.stories.tsx new file mode 100644 index 0000000..21b2e64 --- /dev/null +++ b/stories/bell-icon.stories.tsx @@ -0,0 +1,51 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { BellIcon } from '../components/core/bell-icon'; + +const meta = { + title: 'Core/BellIcon', + component: BellIcon, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], + argTypes: { + isAnimating: { control: 'boolean' }, + startOnHover: { control: 'boolean' }, + animationType: { control: 'radio', options: ['bounce', 'ring', 'shake'] }, + size: { control: { type: 'number', min: 16, max: 128, step: 4 } }, + color: { control: 'color' }, + clapperColor: { control: 'color' }, + className: { control: 'text' }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + isAnimating: false, + startOnHover: true, + size: 48, + animationType: 'bounce', + }, +}; + +export const CustomColors: Story = { + args: { + isAnimating: true, + animationType: 'ring', + size: 48, + color: '#000000', + clapperColor: '#06b6d4', + }, +}; + +export const Shake: Story = { + args: { + isAnimating: true, + animationType: 'shake', + size: 48, + color: '#ef4444', + }, +}; diff --git a/stories/global-icon.stories.tsx b/stories/global-icon.stories.tsx new file mode 100644 index 0000000..0d351c3 --- /dev/null +++ b/stories/global-icon.stories.tsx @@ -0,0 +1,60 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { GlobalIcon } from '../components/core/global-icon'; + +const meta = { + title: 'Core/GlobalIcon', + component: GlobalIcon, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], + argTypes: { + isAnimating: { control: 'boolean' }, + startOnHover: { control: 'boolean' }, + animationType: { control: 'radio', options: ['spin', 'bounce', 'ping'] }, + size: { control: { type: 'number', min: 16, max: 128, step: 4 } }, + color: { control: 'color' }, + pinColor: { control: 'color' }, + className: { control: 'text' }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + isAnimating: false, + startOnHover: true, + size: 48, + animationType: 'bounce', + }, +}; + +export const CustomPins: Story = { + args: { + isAnimating: true, + animationType: 'bounce', + size: 48, + color: '#000000', + pinColor: '#06b6d4', + }, +}; + +export const Spin: Story = { + args: { + isAnimating: true, + animationType: 'spin', + size: 48, + color: '#3b82f6', + }, +}; + +export const Ping: Story = { + args: { + isAnimating: true, + animationType: 'ping', + size: 48, + color: '#ef4444', + }, +}; diff --git a/stories/global-search-icon.stories.tsx b/stories/global-search-icon.stories.tsx new file mode 100644 index 0000000..828d951 --- /dev/null +++ b/stories/global-search-icon.stories.tsx @@ -0,0 +1,51 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { GlobalSearchIcon } from '../components/core/global-search-icon'; + +const meta = { + title: 'Core/GlobalSearchIcon', + component: GlobalSearchIcon, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], + argTypes: { + isAnimating: { control: 'boolean' }, + startOnHover: { control: 'boolean' }, + animationType: { control: 'radio', options: ['rotate', 'search', 'shake'] }, + size: { control: { type: 'number', min: 16, max: 128, step: 4 } }, + color: { control: 'color' }, + globeColor: { control: 'color' }, + className: { control: 'text' }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + isAnimating: false, + startOnHover: true, + size: 48, + animationType: 'rotate', + }, +}; + +export const CustomColors: Story = { + args: { + isAnimating: true, + animationType: 'search', + size: 48, + color: '#000000', + globeColor: '#06b6d4', + }, +}; + +export const Shake: Story = { + args: { + isAnimating: true, + animationType: 'shake', + size: 48, + color: '#ef4444', + }, +}; diff --git a/stories/heart-icon.stories.tsx b/stories/heart-icon.stories.tsx new file mode 100644 index 0000000..f0f27e6 --- /dev/null +++ b/stories/heart-icon.stories.tsx @@ -0,0 +1,51 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { HeartIcon } from '../components/core/heart-icon'; + +const meta = { + title: 'Core/HeartIcon', + component: HeartIcon, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], + argTypes: { + isAnimating: { control: 'boolean' }, + startOnHover: { control: 'boolean' }, + animationType: { control: 'radio', options: ['bounce', 'beat', 'pulse'] }, + size: { control: { type: 'number', min: 16, max: 128, step: 4 } }, + color: { control: 'color' }, + shineColor: { control: 'color' }, + className: { control: 'text' }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + isAnimating: false, + startOnHover: true, + size: 48, + animationType: 'bounce', + }, +}; + +export const Beat: Story = { + args: { + isAnimating: true, + animationType: 'beat', + size: 48, + color: '#000000', + shineColor: '#06b6d4', + }, +}; + +export const Pulse: Story = { + args: { + isAnimating: true, + animationType: 'pulse', + size: 48, + color: '#ef4444', + }, +}; diff --git a/stories/like-icon.stories.tsx b/stories/like-icon.stories.tsx new file mode 100644 index 0000000..3b0f7b2 --- /dev/null +++ b/stories/like-icon.stories.tsx @@ -0,0 +1,51 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { LikeIcon } from '../components/core/like-icon'; + +const meta = { + title: 'Core/LikeIcon', + component: LikeIcon, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], + argTypes: { + isAnimating: { control: 'boolean' }, + startOnHover: { control: 'boolean' }, + animationType: { control: 'radio', options: ['bounce', 'like', 'wiggle'] }, + size: { control: { type: 'number', min: 16, max: 128, step: 4 } }, + color: { control: 'color' }, + cuffColor: { control: 'color' }, + className: { control: 'text' }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + isAnimating: false, + startOnHover: true, + size: 48, + animationType: 'bounce', + }, +}; + +export const CustomColors: Story = { + args: { + isAnimating: true, + animationType: 'like', + size: 48, + color: '#000000', + cuffColor: '#06b6d4', + }, +}; + +export const Wiggle: Story = { + args: { + isAnimating: true, + animationType: 'wiggle', + size: 48, + color: '#ef4444', + }, +}; diff --git a/stories/mail-stack-icon.stories.tsx b/stories/mail-stack-icon.stories.tsx new file mode 100644 index 0000000..98f0852 --- /dev/null +++ b/stories/mail-stack-icon.stories.tsx @@ -0,0 +1,51 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { MailStackIcon } from '../components/core/mail-stack-icon'; + +const meta = { + title: 'Core/MailStackIcon', + component: MailStackIcon, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], + argTypes: { + isAnimating: { control: 'boolean' }, + startOnHover: { control: 'boolean' }, + animationType: { control: 'radio', options: ['bounce', 'slide', 'rotate'] }, + size: { control: { type: 'number', min: 16, max: 128, step: 4 } }, + color: { control: 'color' }, + stackColor: { control: 'color' }, + className: { control: 'text' }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + isAnimating: false, + startOnHover: true, + size: 48, + animationType: 'bounce', + }, +}; + +export const CustomColors: Story = { + args: { + isAnimating: true, + animationType: 'slide', + size: 48, + color: '#000000', + stackColor: '#06b6d4', + }, +}; + +export const Rotate: Story = { + args: { + isAnimating: true, + animationType: 'rotate', + size: 48, + color: '#ef4444', + }, +}; diff --git a/stories/microphone-icon.stories.tsx b/stories/microphone-icon.stories.tsx new file mode 100644 index 0000000..45abcfa --- /dev/null +++ b/stories/microphone-icon.stories.tsx @@ -0,0 +1,51 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { MicrophoneIcon } from '../components/core/microphone-icon'; + +const meta = { + title: 'Core/MicrophoneIcon', + component: MicrophoneIcon, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], + argTypes: { + isAnimating: { control: 'boolean' }, + startOnHover: { control: 'boolean' }, + animationType: { control: 'radio', options: ['bounce', 'shake', 'pulse'] }, + size: { control: { type: 'number', min: 16, max: 128, step: 4 } }, + color: { control: 'color' }, + standColor: { control: 'color' }, + className: { control: 'text' }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + isAnimating: false, + startOnHover: true, + size: 48, + animationType: 'bounce', + }, +}; + +export const CustomColors: Story = { + args: { + isAnimating: true, + animationType: 'shake', + size: 48, + color: '#000000', + standColor: '#06b6d4', + }, +}; + +export const Pulse: Story = { + args: { + isAnimating: true, + animationType: 'pulse', + size: 48, + color: '#ef4444', + }, +}; diff --git a/stories/paper-plane-icon.stories.tsx b/stories/paper-plane-icon.stories.tsx new file mode 100644 index 0000000..4053caa --- /dev/null +++ b/stories/paper-plane-icon.stories.tsx @@ -0,0 +1,51 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { PaperPlaneIcon } from '../components/core/paper-plane-icon'; + +const meta = { + title: 'Core/PaperPlaneIcon', + component: PaperPlaneIcon, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], + argTypes: { + isAnimating: { control: 'boolean' }, + startOnHover: { control: 'boolean' }, + animationType: { control: 'radio', options: ['bounce', 'fly', 'wobble'] }, + size: { control: { type: 'number', min: 16, max: 128, step: 4 } }, + color: { control: 'color' }, + trailColor: { control: 'color' }, + className: { control: 'text' }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + isAnimating: false, + startOnHover: true, + size: 48, + animationType: 'bounce', + }, +}; + +export const CustomColors: Story = { + args: { + isAnimating: true, + animationType: 'fly', + size: 48, + color: '#000000', + trailColor: '#06b6d4', + }, +}; + +export const Wobble: Story = { + args: { + isAnimating: true, + animationType: 'wobble', + size: 48, + color: '#ef4444', + }, +}; diff --git a/stories/share-icon.stories.tsx b/stories/share-icon.stories.tsx new file mode 100644 index 0000000..7cf3bf0 --- /dev/null +++ b/stories/share-icon.stories.tsx @@ -0,0 +1,51 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { ShareIcon } from '../components/core/share-icon'; + +const meta = { + title: 'Core/ShareIcon', + component: ShareIcon, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], + argTypes: { + isAnimating: { control: 'boolean' }, + startOnHover: { control: 'boolean' }, + animationType: { control: 'radio', options: ['bounce', 'pulse', 'rotate'] }, + size: { control: { type: 'number', min: 16, max: 128, step: 4 } }, + color: { control: 'color' }, + dotColor: { control: 'color' }, + className: { control: 'text' }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + isAnimating: false, + startOnHover: true, + size: 48, + animationType: 'bounce', + }, +}; + +export const CustomColors: Story = { + args: { + isAnimating: true, + animationType: 'rotate', + size: 48, + color: '#000000', + dotColor: '#06b6d4', + }, +}; + +export const Pulse: Story = { + args: { + isAnimating: true, + animationType: 'pulse', + size: 48, + color: '#ef4444', + }, +}; diff --git a/stories/tools-icon.stories.tsx b/stories/tools-icon.stories.tsx new file mode 100644 index 0000000..690eba6 --- /dev/null +++ b/stories/tools-icon.stories.tsx @@ -0,0 +1,54 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { ToolsIcon } from '../components/core/tools-icon'; + +const meta = { + title: 'Core/ToolsIcon', + component: ToolsIcon, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], + argTypes: { + isAnimating: { control: 'boolean' }, + startOnHover: { control: 'boolean' }, + animationType: { + control: 'radio', + options: ['bounce', 'wiggle', 'repair'], + }, + size: { control: { type: 'number', min: 16, max: 128, step: 4 } }, + color: { control: 'color' }, + screwdriverColor: { control: 'color' }, + className: { control: 'text' }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + isAnimating: false, + startOnHover: true, + size: 48, + animationType: 'bounce', + }, +}; + +export const CustomColors: Story = { + args: { + isAnimating: true, + animationType: 'repair', + size: 48, + color: '#000000', + screwdriverColor: '#06b6d4', + }, +}; + +export const Wiggle: Story = { + args: { + isAnimating: true, + animationType: 'wiggle', + size: 48, + color: '#ef4444', + }, +}; diff --git a/stories/trash-icon.stories.tsx b/stories/trash-icon.stories.tsx new file mode 100644 index 0000000..37d92e0 --- /dev/null +++ b/stories/trash-icon.stories.tsx @@ -0,0 +1,51 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { TrashIcon } from '../components/core/trash-icon'; + +const meta = { + title: 'Core/TrashIcon', + component: TrashIcon, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], + argTypes: { + isAnimating: { control: 'boolean' }, + startOnHover: { control: 'boolean' }, + animationType: { control: 'radio', options: ['bounce', 'trash', 'shake'] }, + size: { control: { type: 'number', min: 16, max: 128, step: 4 } }, + color: { control: 'color' }, + lidColor: { control: 'color' }, + className: { control: 'text' }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + isAnimating: false, + startOnHover: true, + size: 48, + animationType: 'bounce', + }, +}; + +export const CustomColors: Story = { + args: { + isAnimating: true, + animationType: 'trash', + size: 48, + color: '#000000', + lidColor: '#06b6d4', + }, +}; + +export const Shake: Story = { + args: { + isAnimating: true, + animationType: 'shake', + size: 48, + color: '#ef4444', + }, +}; diff --git a/stories/trophy-icon.stories.tsx b/stories/trophy-icon.stories.tsx new file mode 100644 index 0000000..23fd835 --- /dev/null +++ b/stories/trophy-icon.stories.tsx @@ -0,0 +1,51 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { TrophyIcon } from '../components/core/trophy-icon'; + +const meta = { + title: 'Core/TrophyIcon', + component: TrophyIcon, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], + argTypes: { + isAnimating: { control: 'boolean' }, + startOnHover: { control: 'boolean' }, + animationType: { control: 'radio', options: ['bounce', 'wobble', 'shine'] }, + size: { control: { type: 'number', min: 16, max: 128, step: 4 } }, + color: { control: 'color' }, + starColor: { control: 'color' }, + className: { control: 'text' }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + isAnimating: false, + startOnHover: true, + size: 48, + animationType: 'bounce', + }, +}; + +export const CustomColors: Story = { + args: { + isAnimating: true, + animationType: 'shine', + size: 48, + color: '#000000', + starColor: '#fbbf24', + }, +}; + +export const Wobble: Story = { + args: { + isAnimating: true, + animationType: 'wobble', + size: 48, + color: '#ef4444', + }, +}; diff --git a/stories/user-icon.stories.tsx b/stories/user-icon.stories.tsx new file mode 100644 index 0000000..307498b --- /dev/null +++ b/stories/user-icon.stories.tsx @@ -0,0 +1,51 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { UserIcon } from '../components/core/user-icon'; + +const meta = { + title: 'Core/UserIcon', + component: UserIcon, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], + argTypes: { + isAnimating: { control: 'boolean' }, + startOnHover: { control: 'boolean' }, + animationType: { control: 'radio', options: ['bounce', 'hi', 'pulse'] }, + size: { control: { type: 'number', min: 16, max: 128, step: 4 } }, + color: { control: 'color' }, + userColor: { control: 'color' }, + className: { control: 'text' }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + isAnimating: false, + startOnHover: true, + size: 48, + animationType: 'bounce', + }, +}; + +export const CustomColors: Story = { + args: { + isAnimating: true, + animationType: 'hi', + size: 48, + color: '#000000', + userColor: '#06b6d4', + }, +}; + +export const Pulse: Story = { + args: { + isAnimating: true, + animationType: 'pulse', + size: 48, + color: '#3b82f6', + }, +};