{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "animated-icon",
  "title": "Animated Icon",
  "description": "Any icon, animated with a motion that fits it: bells ring, arrows nudge, gears turn.",
  "dependencies": [],
  "registryDependencies": [
    "utils",
    "https://ui.cuelark.com/r/theme.json",
    "https://ui.cuelark.com/r/motion.json"
  ],
  "files": [
    {
      "path": "src/registry/cuelark/ui/animated-icon.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { gsap, motionScale } from \"@/lib/motion\";\nimport { cn } from \"@/lib/utils\";\n\ntype Animation =\n  | \"auto\"\n  | \"pop\"\n  | \"wiggle\"\n  | \"spin\"\n  | \"bounce\"\n  | \"pulse\"\n  | \"shake\"\n  | \"float\"\n  | \"flip\"\n  | \"tada\"\n  | \"ring\"\n  | \"beat\"\n  | \"nudge-right\"\n  | \"nudge-left\"\n  | \"nudge-up\"\n  | \"nudge-down\"\n  | \"fly\"\n  | \"turn\"\n  | \"spin-back\"\n  | \"twinkle\"\n  | \"zoom\"\n  | \"blink\"\n  | \"wave\"\n  | \"flicker\"\n  | \"grow\"\n  | \"drive\"\n  | \"drop\"\n  | \"nod\"\n  | \"write\"\n  | \"rock\"\n  | \"launch\";\ntype Trigger = \"hover\" | \"click\" | \"mount\" | \"inView\" | \"loop\";\n\ntype AnimatedIconProps = Omit<React.ComponentProps<\"span\">, \"children\"> & {\n  /** Any icon element: react-icons, lucide-react, an installed Cuelark icon or a raw <svg>. */\n  children: React.ReactNode;\n  /** \"auto\" (default) picks a motion that fits the icon from its name: bells ring, arrows nudge, gears turn… */\n  animation?: Animation;\n  /** The icon's name, e.g. \"LuBell\" or \"arrow right\", used by \"auto\". Read from the child component when omitted. */\n  name?: string;\n  trigger?: Trigger;\n  /** Seconds; each animation has a tuned default. */\n  duration?: number;\n};\n\nconst ANIMATIONS = [\"auto\", \"pop\", \"wiggle\", \"spin\", \"bounce\", \"shake\", \"float\", \"flip\", \"tada\", \"ring\", \"twinkle\", \"fly\", \"blink\", \"wave\"] as const;\n\n/** Name → motion. Ordered: the first rule that matches wins. */\nconst RULES: [RegExp, Animation][] = [\n  [/bell|notification|alarm|phone|call|ring/, \"ring\"],\n  [/heart|like|love|favorite/, \"beat\"],\n  [/send|paper ?plane|plane|flight/, \"fly\"],\n  [/rocket|launch/, \"launch\"],\n  [/undo|rotate ?ccw|counter/, \"spin-back\"],\n  [/refresh|sync|reload|rotate|loader|spinner|loading|repeat|redo|replay|update|cycle/, \"spin\"],\n  [/setting|gear|cog|tune|cogs/, \"turn\"],\n  [/wrench|tool|hammer|screwdriver|build|construct/, \"wiggle\"],\n  [/\\b(plus|add|close|x|times|cross|xmark)\\b/, \"turn\"],\n  [/trash|delete|bin|remove|erase/, \"shake\"],\n  [/lock|key|shield|secure|ban|block|warning|alert|error/, \"shake\"],\n  [/check|tick|done|success|verified|approve/, \"pop\"],\n  [/star|sparkle|magic|wand|shine|glitter|award|trophy|medal|crown/, \"twinkle\"],\n  [/search|magnif|zoom|find|scan/, \"zoom\"],\n  [/download|inbox|import|save|arrow ?down|chevron ?down|caret ?down|angle ?down|sort ?down|south/, \"nudge-down\"],\n  [/upload|export|share|arrow ?up|chevron ?up|caret ?up|angle ?up|north|trending ?up/, \"nudge-up\"],\n  [/arrow ?left|chevron ?left|caret ?left|angle ?left|back|previous|prev|west|rewind/, \"nudge-left\"],\n  [/arrow|chevron|caret|angle|next|forward|right|east|login|logout|exit|external|redirect|enter/, \"nudge-right\"],\n  [/eye|view|visib|watch|look/, \"blink\"],\n  [/flag|banner|wave/, \"wave\"],\n  [/fire|flame|zap|bolt|lightning|flash|spark|battery|power|plug|energy/, \"flicker\"],\n  [/chart|graph|bar|analytic|stat|trending|growth|pie|poll/, \"grow\"],\n  [/cart|truck|car|bike|bus|shipping|delivery|train|taxi|vehicle/, \"drive\"],\n  [/pin|map|location|marker|place|navigation|compass/, \"drop\"],\n  [/user|person|profile|account|people|group|team|contact|avatar|smile|face|emoji|mood/, \"nod\"],\n  [/edit|pen|pencil|write|brush|paint|draw|sign|signature|highlight/, \"write\"],\n  [/globe|world|earth|planet|language|translate|internet|coin|dollar|money|currency/, \"flip\"],\n  [/moon|night|dark/, \"rock\"],\n  [/sun|light|bright|day/, \"spin\"],\n  [/cloud|wind|weather|rain|snow|feather|balloon|bubble/, \"float\"],\n  [/music|headphone|volume|speaker|audio|sound|mic|podcast|radio/, \"bounce\"],\n  [/mail|envelope|message|chat|comment|inbox|sms|reply/, \"tada\"],\n  [/gift|present|bug|party|confetti|celebrat/, \"wiggle\"],\n  [/play|video|film|movie|youtube/, \"pop\"],\n  [/clock|time|timer|watch|hourglass|history|calendar|schedule|stopwatch/, \"spin\"],\n];\n\n/** Picks the animation that best fits an icon name (react-icons names like \"LuArrowUpRight\" or plain words). */\nexport function animationFor(name: string | undefined): Exclude<Animation, \"auto\"> {\n  if (!name) return \"pop\";\n  const words = name\n    .replace(/^(Ai|Bi|Bs|Cg|Ci|Di|Fa6?|Fc|Fi|Gi|Go|Gr|Hi2?|Im|Io5?|Lia|Lu|Md|Pi|Ri|Rx|Si|Sl|Tb|Tfi|Ti|Vsc|Wi)(?=[A-Z0-9])/, \"\")\n    .replace(/([a-z0-9])([A-Z])/g, \"$1 $2\")\n    .toLowerCase();\n  for (const [re, a] of RULES) if (re.test(words)) return a as Exclude<Animation, \"auto\">;\n  return \"pop\";\n}\n\n/** Builds the timeline for one play of an animation (scaled by --cl-motion). */\nfunction play(el: HTMLElement, animation: Exclude<Animation, \"auto\">, s: number, duration?: number) {\n  const tl = gsap.timeline();\n  const d = (v: number) => (duration ?? v) / Math.max(0.5, Math.min(s, 1.5));\n  switch (animation) {\n    case \"pop\":\n      tl.to(el, { scale: 1 + 0.16 * s, duration: d(0.14), ease: \"power2.out\" }).to(el, { scale: 1, duration: d(0.55), ease: \"elastic.out(1, 0.5)\" });\n      break;\n    case \"wiggle\":\n      tl.to(el, { keyframes: { rotate: [0, -14 * s, 12 * s, -8 * s, 5 * s, 0] }, duration: d(0.6), ease: \"power1.inOut\" });\n      break;\n    case \"spin\":\n      tl.to(el, { rotate: \"+=360\", duration: d(0.75), ease: \"power2.inOut\" });\n      break;\n    case \"bounce\":\n      tl.to(el, { y: -6 * s, duration: d(0.18), ease: \"power2.out\" }).to(el, { y: 0, duration: d(0.6), ease: \"bounce.out\" });\n      break;\n    case \"pulse\":\n      tl.to(el, { scale: 1 + 0.15 * s, opacity: 0.7, duration: d(0.2), ease: \"power1.out\" }).to(el, { scale: 1, opacity: 1, duration: d(0.35), ease: \"power1.in\" });\n      break;\n    case \"shake\":\n      tl.to(el, { keyframes: { x: [0, -3 * s, 3 * s, -2 * s, 2 * s, 0] }, duration: d(0.4), ease: \"none\" });\n      break;\n    case \"float\":\n      tl.to(el, { y: -4 * s, duration: d(0.9), ease: \"sine.inOut\", yoyo: true, repeat: 1 });\n      break;\n    case \"flip\":\n      tl.fromTo(el, { rotateY: 0 }, { rotateY: 360, duration: d(0.7), ease: \"cue\", transformPerspective: 200 });\n      break;\n    case \"tada\":\n      tl.to(el, { keyframes: { scale: [1, 0.9, 1.15, 1.15, 1.15, 1], rotate: [0, -6, 6, -6, 6, 0] }, duration: d(0.8), ease: \"power1.inOut\" });\n      break;\n    case \"ring\":\n      tl.set(el, { transformOrigin: \"50% 8%\" }).to(el, { keyframes: { rotate: [0, 18 * s, -16 * s, 12 * s, -8 * s, 4 * s, 0] }, duration: d(0.8), ease: \"power1.out\" });\n      break;\n    case \"beat\":\n      tl.to(el, { keyframes: { scale: [1, 1 + 0.22 * s, 1, 1 + 0.12 * s, 1] }, duration: d(0.7), ease: \"power1.inOut\" });\n      break;\n    case \"nudge-right\":\n    case \"nudge-left\":\n    case \"nudge-up\":\n    case \"nudge-down\": {\n      const axis = animation === \"nudge-right\" || animation === \"nudge-left\" ? \"x\" : \"y\";\n      const sign = animation === \"nudge-right\" || animation === \"nudge-down\" ? 1 : -1;\n      tl.to(el, { [axis]: 4 * s * sign, duration: d(0.16), ease: \"power2.out\" })\n        .to(el, { [axis]: 0, duration: d(0.16), ease: \"power2.in\" })\n        .to(el, { [axis]: 2.5 * s * sign, duration: d(0.14), ease: \"power2.out\" })\n        .to(el, { [axis]: 0, duration: d(0.3), ease: \"elastic.out(1, 0.5)\" });\n      break;\n    }\n    case \"fly\":\n      tl.to(el, { x: 14 * s, y: -14 * s, opacity: 0, rotate: -8, duration: d(0.28), ease: \"power2.in\" })\n        .set(el, { x: -14 * s, y: 14 * s })\n        .to(el, { x: 0, y: 0, opacity: 1, rotate: 0, duration: d(0.4), ease: \"cue\" });\n      break;\n    case \"launch\":\n      tl.to(el, { y: 3 * s, duration: d(0.12), ease: \"power1.out\" })\n        .to(el, { y: -16 * s, opacity: 0, duration: d(0.3), ease: \"power3.in\" })\n        .set(el, { y: 12 * s })\n        .to(el, { y: 0, opacity: 1, duration: d(0.4), ease: \"cue\" });\n      break;\n    case \"turn\":\n      // Accumulates (90°, 180°…) so a turned gear or plus never snaps back.\n      tl.to(el, { rotate: \"+=90\", duration: d(0.55), ease: \"back.out(1.7)\" });\n      break;\n    case \"spin-back\":\n      tl.to(el, { rotate: \"-=360\", duration: d(0.75), ease: \"power2.inOut\" });\n      break;\n    case \"twinkle\":\n      tl.to(el, { scale: 1 + 0.18 * s, rotate: 16 * s, duration: d(0.22), ease: \"power2.out\" }).to(el, { scale: 1, rotate: 0, duration: d(0.7), ease: \"elastic.out(1, 0.45)\" });\n      break;\n    case \"zoom\":\n      tl.to(el, { scale: 1 + 0.18 * s, rotate: -12 * s, duration: d(0.22), ease: \"power2.out\" }).to(el, { scale: 1, rotate: 0, duration: d(0.45), ease: \"back.out(2)\" });\n      break;\n    case \"blink\":\n      tl.to(el, { scaleY: 0.1, duration: d(0.09), ease: \"power2.in\" }).to(el, { scaleY: 1, duration: d(0.16), ease: \"power2.out\" });\n      break;\n    case \"wave\":\n      tl.set(el, { transformOrigin: \"0% 100%\" }).to(el, { keyframes: { skewX: [0, -12 * s, 10 * s, -6 * s, 0], rotate: [0, -4 * s, 3 * s, 0, 0] }, duration: d(0.8), ease: \"sine.inOut\" });\n      break;\n    case \"flicker\":\n      tl.to(el, { keyframes: { opacity: [1, 0.35, 1, 0.6, 1], scale: [1, 1.08, 0.96, 1.05, 1] }, duration: d(0.5), ease: \"none\" });\n      break;\n    case \"grow\":\n      tl.set(el, { transformOrigin: \"50% 100%\" }).fromTo(el, { scaleY: 0.4 }, { scaleY: 1, duration: d(0.55), ease: \"elastic.out(1, 0.5)\" });\n      break;\n    case \"drive\":\n      tl.to(el, { x: 18 * s, opacity: 0, duration: d(0.3), ease: \"power2.in\" })\n        .set(el, { x: -18 * s })\n        .to(el, { x: 0, opacity: 1, duration: d(0.45), ease: \"cue\" });\n      break;\n    case \"drop\":\n      tl.fromTo(el, { y: -10 * s }, { y: 0, duration: d(0.6), ease: \"bounce.out\" });\n      break;\n    case \"nod\":\n      tl.to(el, { keyframes: { y: [0, -2.5 * s, 0, -1.5 * s, 0], rotate: [0, -5 * s, 0, 3 * s, 0] }, duration: d(0.6), ease: \"power1.inOut\" });\n      break;\n    case \"write\":\n      tl.set(el, { transformOrigin: \"10% 90%\" }).to(el, { keyframes: { rotate: [0, -14 * s, -6 * s, -14 * s, 0], x: [0, 2, 4, 6, 0] }, duration: d(0.7), ease: \"power1.inOut\" });\n      break;\n    case \"rock\":\n      tl.to(el, { keyframes: { rotate: [0, -18 * s, 12 * s, -6 * s, 0] }, duration: d(0.8), ease: \"sine.inOut\" });\n      break;\n  }\n  return tl;\n}\n\n/**\n * Brings any icon to life with GSAP: pop, wiggle, spin, bounce, pulse, shake, float, flip, tada, ring and more. Plays on hover, click, mount, when scrolled into view, or on a loop; follows\n * --cl-motion and stays still for reduced motion.\n */\nfunction AnimatedIcon({ children, animation: requested = \"auto\", name, trigger = \"hover\", duration, className, onPointerEnter, onClick, ...props }: AnimatedIconProps) {\n  const ref = React.useRef<HTMLSpanElement>(null);\n  const childName = React.isValidElement(children) ? ((children.type as { displayName?: string; name?: string }).displayName ?? (children.type as { name?: string }).name) : undefined;\n  const animation = requested === \"auto\" ? animationFor(name ?? childName) : requested;\n  const tl = React.useRef<gsap.core.Timeline | null>(null);\n\n  const resolved = animation;\n\n  const run = React.useCallback(() => {\n    const el = ref.current;\n    const s = el ? motionScale(el) : 0;\n    if (!el || !s || tl.current?.isActive()) return;\n    tl.current = play(el, resolved, s, duration);\n  }, [resolved, duration]);\n\n  React.useEffect(() => {\n    const el = ref.current;\n    if (!el || !motionScale(el)) return;\n    if (trigger === \"mount\") run();\n    if (trigger === \"loop\") {\n      const loop = () => {\n        tl.current = play(el, resolved, motionScale(el), duration);\n        tl.current.eventCallback(\"onComplete\", () => void gsap.delayedCall(0.6, loop));\n      };\n      loop();\n      return () => {\n        tl.current?.kill();\n        gsap.killTweensOf(loop);\n      };\n    }\n    if (trigger === \"inView\") {\n      const io = new IntersectionObserver(([e]) => e?.isIntersecting && (run(), io.disconnect()), { threshold: 0.6 });\n      io.observe(el);\n      return () => io.disconnect();\n    }\n  }, [trigger, resolved, duration, run]);\n\n  return (\n    <span\n      ref={ref}\n      data-slot=\"animated-icon\"\n      data-animation={animation}\n      data-resolved={resolved}\n      className={cn(\"inline-flex shrink-0 items-center justify-center will-change-transform [&>svg]:size-[1em]\", className)}\n      onPointerEnter={(e) => {\n        onPointerEnter?.(e);\n        if (trigger === \"hover\") run();\n      }}\n      onClick={(e) => {\n        onClick?.(e);\n        if (trigger === \"click\") run();\n      }}\n      {...props}\n    >\n      {children}\n    </span>\n  );\n}\n\nexport { AnimatedIcon, ANIMATIONS };\nexport type { AnimatedIconProps, Animation as IconAnimation, Trigger as IconTrigger };\n",
      "type": "registry:ui"
    }
  ],
  "docs": "Docs and examples: https://ui.cuelark.com/components/animated-icon",
  "categories": [
    "effects"
  ],
  "type": "registry:ui"
}