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)}
+ >
+
+
+ );
+}
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)}
+ >
+
+
+ );
+}
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)}
+ >
+
+
+ );
+}
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)}
+ >
+
+
+ );
+}
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)}
+ >
+
+
+ );
+}
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)}
+ >
+
+
+ );
+}
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)}
+ >
+
+
+ );
+}
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)}
+ >
+
+
+ );
+}
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)}
+ >
+
+
+ );
+}
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)}
+ >
+
+ {/* FINAL CLEAN VERSION: Overwriting the whole SVG content with verified paths */}
+
+
+ );
+}
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)}
+ >
+
+
+ );
+}
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)}
+ >
+
+
+ );
+}
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)}
+ >
+
+
+ );
+}
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
\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 );\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 );\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
\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
\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 );\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 );\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
\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
\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
\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
\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 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
\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}\n",
+ "content": "'use client';\n\nimport { PhoneIcon } from '@/components/core/phone-icon';\n\nexport default function PhoneIconBasic() {\n return (\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 );\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 {/* FINAL CLEAN VERSION: Overwriting the whole SVG content with verified paths */}\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
\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 );\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
\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',
+ },
+};