18 lines
9.4 KiB
JSON
18 lines
9.4 KiB
JSON
{
|
|
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
"name": "icon",
|
|
"type": "registry:ui",
|
|
"title": "Icon",
|
|
"description": "Base component to use animated icons.",
|
|
"dependencies": [
|
|
"motion"
|
|
],
|
|
"files": [
|
|
{
|
|
"path": "registry/icons/icon/index.tsx",
|
|
"content": "'use client';\n\nimport * as React from 'react';\nimport {\n SVGMotionProps,\n useAnimation,\n type LegacyAnimationControls,\n type Variants,\n} from 'motion/react';\n\nimport { cn } from '@/lib/utils';\n\nconst staticAnimations = {\n path: {\n initial: { pathLength: 1, opacity: 1 },\n animate: {\n pathLength: [0.05, 1],\n opacity: [0, 1],\n transition: {\n duration: 0.8,\n ease: 'easeInOut',\n opacity: { duration: 0.01 },\n },\n },\n } as Variants,\n 'path-loop': {\n initial: { pathLength: 1, opacity: 1 },\n animate: {\n pathLength: [1, 0.05, 1],\n opacity: [1, 0, 1],\n transition: {\n duration: 1.6,\n ease: 'easeInOut',\n opacity: { duration: 0.01 },\n },\n },\n } as Variants,\n} as const;\n\ntype StaticAnimations = keyof typeof staticAnimations;\ntype TriggerProp<T = string> = boolean | StaticAnimations | T;\n\ninterface AnimateIconContextValue {\n controls: LegacyAnimationControls | undefined;\n animation: StaticAnimations | string;\n loop: boolean;\n loopDelay: number;\n}\n\ninterface DefaultIconProps<T = string> {\n animate?: TriggerProp<T>;\n onAnimateChange?: (\n value: boolean,\n animation: StaticAnimations | string,\n ) => void;\n animateOnHover?: TriggerProp<T>;\n animateOnTap?: TriggerProp<T>;\n animation?: T | StaticAnimations;\n loop?: boolean;\n loopDelay?: number;\n onAnimateStart?: () => void;\n onAnimateEnd?: () => void;\n}\n\ninterface AnimateIconProps<T = string> extends DefaultIconProps<T> {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n children: React.ReactElement<any, any>;\n}\n\ninterface IconProps<T>\n extends DefaultIconProps<T>,\n Omit<\n SVGMotionProps<SVGSVGElement>,\n 'animate' | 'onAnimationStart' | 'onAnimationEnd'\n > {\n size?: number;\n}\n\ninterface IconWrapperProps<T> extends IconProps<T> {\n icon: React.ComponentType<IconProps<T>>;\n}\n\nconst AnimateIconContext = React.createContext<AnimateIconContextValue | null>(\n null,\n);\n\nfunction useAnimateIconContext() {\n const context = React.useContext(AnimateIconContext);\n if (!context)\n return {\n controls: undefined,\n animation: 'default',\n loop: false,\n loopDelay: 0,\n };\n return context;\n}\n\nfunction AnimateIcon({\n animate,\n onAnimateChange,\n animateOnHover,\n animateOnTap,\n animation = 'default',\n loop = false,\n loopDelay = 0,\n onAnimateStart,\n onAnimateEnd,\n children,\n}: AnimateIconProps) {\n const controls = useAnimation();\n const [localAnimate, setLocalAnimate] = React.useState(!!animate);\n const currentAnimation = React.useRef(animation);\n\n const startAnimation = React.useCallback(\n (trigger: TriggerProp) => {\n currentAnimation.current =\n typeof trigger === 'string' ? trigger : animation;\n setLocalAnimate(true);\n },\n [animation],\n );\n\n const stopAnimation = React.useCallback(() => {\n setLocalAnimate(false);\n }, []);\n\n React.useEffect(() => {\n currentAnimation.current =\n typeof animate === 'string' ? animate : animation;\n setLocalAnimate(!!animate);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [animate]);\n\n React.useEffect(\n () => onAnimateChange?.(localAnimate, currentAnimation.current),\n [localAnimate, onAnimateChange],\n );\n\n React.useEffect(() => {\n if (localAnimate) onAnimateStart?.();\n controls.start(localAnimate ? 'animate' : 'initial').then(() => {\n if (localAnimate) onAnimateEnd?.();\n });\n }, [localAnimate, controls, onAnimateStart, onAnimateEnd]);\n\n const handleMouseEnter = (e: MouseEvent) => {\n if (animateOnHover) startAnimation(animateOnHover);\n children.props?.onMouseEnter?.(e);\n };\n const handleMouseLeave = (e: MouseEvent) => {\n if (animateOnHover || animateOnTap) stopAnimation();\n children.props?.onMouseLeave?.(e);\n };\n const handlePointerDown = (e: PointerEvent) => {\n if (animateOnTap) startAnimation(animateOnTap);\n children.props?.onPointerDown?.(e);\n };\n const handlePointerUp = (e: PointerEvent) => {\n if (animateOnTap) stopAnimation();\n children.props?.onPointerUp?.(e);\n };\n\n const child = React.Children.only(children);\n const cloned = React.cloneElement(child, {\n onMouseEnter: handleMouseEnter,\n onMouseLeave: handleMouseLeave,\n onPointerDown: handlePointerDown,\n onPointerUp: handlePointerUp,\n });\n\n return (\n <AnimateIconContext.Provider\n value={{\n controls,\n animation: currentAnimation.current,\n loop,\n loopDelay,\n }}\n >\n {cloned}\n </AnimateIconContext.Provider>\n );\n}\n\nconst pathClassName =\n \"[&_[stroke-dasharray='1px_1px']]:![stroke-dasharray:1px_0px]\";\n\nfunction IconWrapper<T extends string>({\n size = 28,\n animation: animationProp,\n animate,\n onAnimateChange,\n animateOnHover = false,\n animateOnTap = false,\n icon: IconComponent,\n loop = false,\n loopDelay = 0,\n onAnimateStart,\n onAnimateEnd,\n className,\n ...props\n}: IconWrapperProps<T>) {\n const context = React.useContext(AnimateIconContext);\n\n if (context) {\n const {\n controls,\n animation: parentAnimation,\n loop: parentLoop,\n loopDelay: parentLoopDelay,\n } = context;\n const animationToUse = animationProp ?? parentAnimation;\n const loopToUse = loop || parentLoop;\n const loopDelayToUse = loopDelay || parentLoopDelay;\n\n return (\n <AnimateIconContext.Provider\n value={{\n controls,\n animation: animationToUse,\n loop: loopToUse,\n loopDelay: loopDelayToUse,\n }}\n >\n <IconComponent\n size={size}\n className={cn(\n className,\n (animationToUse === 'path' || animationToUse === 'path-loop') &&\n pathClassName,\n )}\n {...props}\n />\n </AnimateIconContext.Provider>\n );\n }\n\n if (\n animate !== undefined ||\n onAnimateChange !== undefined ||\n animateOnHover ||\n animateOnTap ||\n animationProp\n ) {\n return (\n <AnimateIcon\n animate={animate}\n onAnimateChange={onAnimateChange}\n animateOnHover={animateOnHover}\n animateOnTap={animateOnTap}\n animation={animationProp}\n loop={loop}\n loopDelay={loopDelay}\n onAnimateStart={onAnimateStart}\n onAnimateEnd={onAnimateEnd}\n >\n <IconComponent\n size={size}\n className={cn(\n className,\n (animationProp === 'path' || animationProp === 'path-loop') &&\n pathClassName,\n )}\n {...props}\n />\n </AnimateIcon>\n );\n }\n\n return (\n <IconComponent\n size={size}\n className={cn(\n className,\n (animationProp === 'path' || animationProp === 'path-loop') &&\n pathClassName,\n )}\n {...props}\n />\n );\n}\n\nfunction getVariants<\n V extends { default: T; [key: string]: T },\n T extends Record<string, Variants>,\n>(animations: V): T {\n // eslint-disable-next-line react-hooks/rules-of-hooks\n const { animation: animationType, loop, loopDelay } = useAnimateIconContext();\n\n let result: T;\n\n if (animationType in staticAnimations) {\n const variant = staticAnimations[animationType as StaticAnimations];\n result = {} as T;\n for (const key in animations.default) {\n if (\n (animationType === 'path' || animationType === 'path-loop') &&\n key.includes('group')\n )\n continue;\n result[key] = variant as T[Extract<keyof T, string>];\n }\n } else {\n result = (animations[animationType as keyof V] as T) ?? animations.default;\n }\n\n if (loop) {\n for (const key in result) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const state = result[key] as any;\n const transition = state.animate?.transition;\n if (!transition) continue;\n\n const hasNestedKeys = Object.values(transition).some(\n (v) =>\n typeof v === 'object' &&\n v !== null &&\n ('ease' in v || 'duration' in v || 'times' in v),\n );\n\n if (hasNestedKeys) {\n for (const prop in transition) {\n const subTrans = transition[prop];\n if (typeof subTrans === 'object' && subTrans !== null) {\n transition[prop] = {\n ...subTrans,\n repeat: Infinity,\n repeatType: 'loop',\n repeatDelay: loopDelay,\n };\n }\n }\n } else {\n state.animate.transition = {\n ...transition,\n repeat: Infinity,\n repeatType: 'loop',\n repeatDelay: loopDelay,\n };\n }\n }\n }\n\n return result;\n}\n\nexport {\n pathClassName,\n staticAnimations,\n AnimateIcon,\n IconWrapper,\n useAnimateIconContext,\n getVariants,\n type IconProps,\n type IconWrapperProps,\n type AnimateIconProps,\n type AnimateIconContextValue,\n};\n",
|
|
"type": "registry:ui",
|
|
"target": "components/animate-ui/icons/icon.tsx"
|
|
}
|
|
]
|
|
} |