{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "animated-tooltip",
  "type": "registry:ui",
  "dependencies": [
    "framer-motion"
  ],
  "files": [
    {
      "path": "components/ui/animated-tooltip.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { useState, useId } from \"react\";\nimport { AnimatePresence, motion, type Variants } from \"framer-motion\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * Animated Tooltip\n * Bouncy, playful tooltip shapes with SVG bubbles and spring animations.\n * Inspired by Codrops' \"Tooltip Animations\" (originally anime.js), rebuilt\n * on framer-motion with theme-adaptive colors.\n */\n\nexport type AnimatedTooltipVariant =\n  | \"cora\"\n  | \"smaug\"\n  | \"dori\"\n  | \"gram\"\n  | \"indis\"\n  | \"malva\"\n  | \"sadoc\";\n\ninterface VariantConfig {\n  /** Bubble size in px. */\n  width: number;\n  height: number;\n  /** Distance of the bubble above the trigger (CSS `bottom`). */\n  bottom: string;\n  /** transform-origin for the entrance animation. */\n  transformOrigin: string;\n  /** SVG path(s) rendered inside a `0 0 400 300` viewBox. */\n  shape: (fill: string) => React.ReactNode;\n  /** Bubble (base) entrance/exit animation, transitions embedded per state. */\n  base: Variants;\n  /** Inner content entrance/exit animation, transitions embedded per state. */\n  content: Variants;\n  /** Extra styles for the content wrapper. */\n  contentStyle?: React.CSSProperties;\n}\n\nconst EASE_OUT_QUINT: [number, number, number, number] = [0.22, 1, 0.36, 1];\nconst EASE_IN: [number, number, number, number] = [0.55, 0, 1, 0.45];\n\nconst VARIANTS: Record<AnimatedTooltipVariant, VariantConfig> = {\n  // Blobby heart \u2014 scales up while un-rotating.\n  cora: {\n    width: 232,\n    height: 174,\n    bottom: \"calc(100% + 0.5rem)\",\n    transformOrigin: \"50% 100%\",\n    shape: (fill) => (\n      <path\n        d=\"M 199,21.9 C 152,22.2 109,35.7 78.8,57.4 48,79.1 29,109 29,142 29,172 45.9,201 73.6,222 101,243 140,258 183,260 189,270 200,282 200,282 200,282 211,270 217,260 261,258 299,243 327,222 354,201 371,172 371,142 371,109 352,78.7 321,57 290,35.3 247,21.9 199,21.9 Z\"\n        fill={fill}\n      />\n    ),\n    base: {\n      initial: { scale: 0, rotate: -180, opacity: 0 },\n      animate: { scale: 1, rotate: 0, opacity: 1, transition: { duration: 0.6, ease: EASE_OUT_QUINT } },\n      exit: { scale: 0, opacity: 0, transition: { duration: 0.18, ease: EASE_IN } },\n    },\n    content: {\n      initial: { y: 20, opacity: 0 },\n      animate: { y: 0, opacity: 1, transition: { duration: 0.3, delay: 0.25, ease: EASE_OUT_QUINT } },\n      exit: { y: 20, opacity: 0, transition: { duration: 0.1, ease: EASE_IN } },\n    },\n    contentStyle: { marginBottom: \"0.75em\" },\n  },\n\n  // Rounded pill with a downward pointer \u2014 tips in with a slide.\n  smaug: {\n    width: 240,\n    height: 180,\n    bottom: \"calc(100% - 0.25rem)\",\n    transformOrigin: \"50% 100%\",\n    shape: (fill) => (\n      <path\n        d=\"M 314,100 C 313,100 312,100 311,100 L 89.5,100 C 55.9,100 29.1,121 29.1,150 29.1,178 53.1,201 89.5,201 L 184,201 200,223 217,201 311,201 C 344,201 371,178 371,150 371,122 346,99 314,100 Z\"\n        fill={fill}\n      />\n    ),\n    base: {\n      initial: { rotate: 35, opacity: 0 },\n      animate: { rotate: 0, opacity: 1, transition: { duration: 0.2, ease: \"easeOut\" } },\n      exit: { rotate: -35, opacity: 0, transition: { duration: 0.2, ease: EASE_IN } },\n    },\n    content: {\n      initial: { x: 50, rotate: 6, opacity: 0 },\n      animate: { x: 0, rotate: 0, opacity: 1, transition: { type: \"spring\", stiffness: 260, damping: 12, delay: 0.05 } },\n      exit: { x: -30, rotate: -6, opacity: 0, transition: { duration: 0.2, ease: EASE_IN } },\n    },\n  },\n\n  // Banner ribbon with a pointer \u2014 drops in with elastic bounce.\n  dori: {\n    width: 240,\n    height: 180,\n    bottom: \"calc(100% - 0.25rem)\",\n    transformOrigin: \"50% 0%\",\n    shape: (fill) => (\n      <path\n        d=\"M 22,108 22,236 C 22,236 64,216 103,212 142,208 184,212 184,212 L 200,229 216,212 C 216,212 258,207 297,212 336,217 378,236 378,236 L 378,108 C 378,108 318,83.7 200,83.7 82,83.7 22,108 22,108 Z\"\n        fill={fill}\n      />\n    ),\n    base: {\n      initial: { y: -60, scale: 0.5, opacity: 0 },\n      animate: { y: 0, scale: 1, opacity: 1, transition: { type: \"spring\", bounce: 0.5, duration: 0.8 } },\n      exit: { y: -60, scale: 0.5, opacity: 0, transition: { duration: 0.2, ease: EASE_IN } },\n    },\n    content: {\n      initial: { y: 20, opacity: 0 },\n      animate: { y: 0, opacity: 1, transition: { duration: 0.3, delay: 0.1, ease: EASE_OUT_QUINT } },\n      exit: { y: 20, opacity: 0, transition: { duration: 0.1, ease: EASE_IN } },\n    },\n    contentStyle: { marginBottom: \"0.75em\" },\n  },\n\n  // Wavy blob \u2014 squashes horizontally on entry.\n  gram: {\n    width: 240,\n    height: 180,\n    bottom: \"calc(100% - 0.25rem)\",\n    transformOrigin: \"50% 100%\",\n    shape: (fill) => (\n      <path\n        d=\"M 92.4,79 C 136,79 154,115 200,116 246,117 263,80.4 308,79 353,77.6 381,111 381,150 381,189 346,220 308,221 270,222 236,188 200,188 164,188 130,222 92.4,221 54.4,220 19,189 19,150 19,111 48.6,79 92.4,79 Z\"\n        fill={fill}\n      />\n    ),\n    base: {\n      initial: { scaleX: 1.2, opacity: 0 },\n      animate: { scaleX: 1, opacity: 1, transition: { duration: 0.4, ease: EASE_OUT_QUINT } },\n      exit: { scaleX: 1.1, scaleY: 0.9, opacity: 0, transition: { duration: 0.2, ease: EASE_IN } },\n    },\n    content: {\n      initial: { scale: 0.8, opacity: 0 },\n      animate: { scale: 1, opacity: 1, transition: { duration: 0.3, delay: 0.1, ease: EASE_OUT_QUINT } },\n      exit: { scale: 0.8, opacity: 0, transition: { duration: 0.15, ease: EASE_IN } },\n    },\n  },\n\n  // Rounded rectangle \u2014 rises from below with a spring.\n  indis: {\n    width: 240,\n    height: 180,\n    bottom: \"calc(100% + 0.25rem)\",\n    transformOrigin: \"50% 100%\",\n    shape: (fill) => (\n      <path\n        d=\"M 44.5,24 C 148,24 252,24 356,24 367,24 376,32.9 376,44 L 376,256 C 376,267 367,276 356,276 252,276 148,276 44.5,276 33.4,276 24.5,267 24.5,256 L 24.5,44 C 24.5,32.9 33.4,24 44.5,24 Z\"\n        fill={fill}\n      />\n    ),\n    base: {\n      initial: { y: 100, scaleX: 0.3, scaleY: 1.3, opacity: 0 },\n      animate: { y: 0, scaleX: 1, scaleY: 1, opacity: 1, transition: { type: \"spring\", bounce: 0.45, duration: 0.9 } },\n      exit: { y: 100, scaleX: 0, scaleY: 1.5, opacity: 0, transition: { duration: 0.25, ease: EASE_IN } },\n    },\n    content: {\n      initial: { y: 10, opacity: 0 },\n      animate: { y: 0, opacity: 1, transition: { duration: 0.3, delay: 0.08, ease: \"easeOut\" } },\n      exit: { y: -20, opacity: 0, transition: { duration: 0.15, ease: EASE_IN } },\n    },\n  },\n\n  // Spiky starburst \u2014 pops in with a jiggle.\n  malva: {\n    width: 220,\n    height: 165,\n    bottom: \"calc(100% + 0.25rem)\",\n    transformOrigin: \"50% 100%\",\n    shape: (fill) => (\n      <path\n        d=\"M 94.9,90.2 101,30.7 163,72.3 229,17.7 263,68.2 319,55.9 315,102 375,144 316,175 340,228 265,220 251,263 180,233 143,282 98.9,218 57.5,236 82,189 25,170 82.8,141 48.7,93.7 Z\"\n        fill={fill}\n      />\n    ),\n    base: {\n      initial: { scale: 0, rotate: -20, opacity: 0 },\n      animate: { scale: [0, 1.15, 1], rotate: [-20, 6, 0], opacity: 1, transition: { duration: 0.7, ease: EASE_OUT_QUINT, times: [0, 0.6, 1] } },\n      exit: { scale: 0, opacity: 0, transition: { duration: 0.18, ease: EASE_IN } },\n    },\n    content: {\n      initial: { scale: 0.7, opacity: 0 },\n      animate: { scale: 1, opacity: 1, transition: { duration: 0.3, delay: 0.25, ease: EASE_OUT_QUINT } },\n      exit: { scale: 0.7, opacity: 0, transition: { duration: 0.1, ease: EASE_IN } },\n    },\n    contentStyle: { width: \"55%\" },\n  },\n\n  // Outlined speech bubble \u2014 springs up from the trigger.\n  sadoc: {\n    width: 240,\n    height: 180,\n    bottom: \"calc(100% + 0.5rem)\",\n    transformOrigin: \"50% 100%\",\n    shape: (fill) => (\n      <path\n        d=\"M 32.1,42.7 54.5,257 185,257 193,269 200,282 207,269 214,257 342,257 368,23.9 Z\"\n        fill={fill}\n        stroke=\"var(--muted-foreground)\"\n        strokeWidth={3}\n        strokeLinejoin=\"round\"\n      />\n    ),\n    base: {\n      initial: { y: -40, opacity: 0 },\n      animate: { y: 0, opacity: 1, transition: { type: \"spring\", bounce: 0.5, duration: 0.8 } },\n      exit: { y: -40, opacity: 0, transition: { duration: 0.2, ease: EASE_IN } },\n    },\n    content: {\n      initial: { y: 20, opacity: 0 },\n      animate: { y: 0, opacity: 1, transition: { type: \"spring\", bounce: 0.4, duration: 0.8, delay: 0.2 } },\n      exit: { y: 20, opacity: 0, transition: { duration: 0.15, ease: EASE_IN } },\n    },\n    contentStyle: { marginBottom: \"1.25em\" },\n  },\n};\n\nexport interface AnimatedTooltipProps {\n  /** The trigger label (usually a word or short phrase). */\n  children: React.ReactNode;\n  /** The tooltip body content revealed on hover/focus. */\n  content: React.ReactNode;\n  /** Which shape + animation style to use. Defaults to \"cora\". */\n  variant?: AnimatedTooltipVariant;\n  /** Trigger text color while the tooltip is open. Defaults to a soft green. */\n  accentColor?: string;\n  /** Bubble fill color. Defaults to the theme foreground token. */\n  shapeColor?: string;\n  /** Tooltip text color. Defaults to the theme background token. */\n  textColor?: string;\n  /** Additional classes for the inline wrapper. */\n  className?: string;\n}\n\nexport function AnimatedTooltip({\n  children,\n  content,\n  variant = \"cora\",\n  accentColor = \"#6fbb95\",\n  shapeColor = \"var(--foreground)\",\n  textColor = \"var(--background)\",\n  className,\n}: AnimatedTooltipProps) {\n  const [open, setOpen] = useState(false);\n  const cfg = VARIANTS[variant] ?? VARIANTS.cora;\n  const id = useId().replace(/:/g, \"\");\n\n  return (\n    <span\n      className={cn(\"relative inline-block\", className)}\n      onMouseEnter={() => setOpen(true)}\n      onMouseLeave={() => setOpen(false)}\n      onFocus={() => setOpen(true)}\n      onBlur={() => setOpen(false)}\n    >\n      <motion.span\n        role=\"button\"\n        tabIndex={0}\n        aria-describedby={open ? id : undefined}\n        className=\"cursor-pointer select-none inline-block px-3 py-2 font-medium\"\n        animate={{ color: open ? accentColor : \"var(--foreground)\" }}\n        transition={{ duration: 0.3, ease: \"easeOut\" }}\n      >\n        {children}\n      </motion.span>\n\n      {/* Static anchor keeps the bubble centered above the trigger;\n          the inner motion element only handles the entrance transforms. */}\n      <span\n        aria-hidden={!open}\n        style={{\n          position: \"absolute\",\n          bottom: cfg.bottom,\n          left: \"50%\",\n          width: cfg.width,\n          height: cfg.height,\n          marginLeft: -cfg.width / 2,\n          pointerEvents: \"none\",\n          zIndex: 50,\n        }}\n      >\n        <AnimatePresence>\n          {open && (\n            <motion.span\n              key=\"base\"\n              id={id}\n              role=\"tooltip\"\n              variants={cfg.base}\n              initial=\"initial\"\n              animate=\"animate\"\n              exit=\"exit\"\n              style={{\n                position: \"absolute\",\n                inset: 0,\n                display: \"flex\",\n                alignItems: \"center\",\n                justifyContent: \"center\",\n                transformOrigin: cfg.transformOrigin,\n              }}\n            >\n              <svg\n                viewBox=\"0 0 400 300\"\n                preserveAspectRatio=\"xMidYMid meet\"\n                style={{ position: \"absolute\", inset: 0, width: \"100%\", height: \"100%\" }}\n                aria-hidden=\"true\"\n              >\n                {cfg.shape(shapeColor)}\n              </svg>\n              <motion.span\n                variants={cfg.content}\n                initial=\"initial\"\n                animate=\"animate\"\n                exit=\"exit\"\n                style={{\n                  position: \"relative\",\n                  width: \"65%\",\n                  textAlign: \"center\",\n                  fontSize: \"0.85rem\",\n                  lineHeight: 1.4,\n                  color: textColor,\n                  ...cfg.contentStyle,\n                }}\n              >\n                {content}\n              </motion.span>\n            </motion.span>\n          )}\n        </AnimatePresence>\n      </span>\n    </span>\n  );\n}\n\nexport default AnimatedTooltip;\n",
      "type": "registry:ui",
      "target": "components/ui/animated-tooltip.tsx"
    }
  ]
}