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