{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "animated-tooltip",
  "title": "Animated Tooltip",
  "description": "A self-contained hover/focus tooltip with a spring pop-in, reduced-motion fallback and an arrow callout — no Radix dependency.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/registry/ui/animated-tooltip.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\nimport { cn } from \"@/lib/utils\"\n\nexport interface AnimatedTooltipProps {\n  /** Tooltip bubble content. */\n  content: React.ReactNode\n  /** Which side of the trigger the bubble appears on. */\n  side?: \"top\" | \"bottom\"\n  /** The trigger — a single focusable element (button, link, or a tabIndex'd\n   * element). The tooltip's id is injected into it via aria-describedby so\n   * screen readers announce the bubble from the element that actually holds\n   * focus (ARIA tooltip pattern), not from the positioning wrapper. */\n  children: React.ReactElement\n  /** Applied to the tooltip bubble, not the trigger wrapper. */\n  className?: string\n}\n\nexport function AnimatedTooltip({ content, side = \"top\", children, className }: AnimatedTooltipProps) {\n  const [hovered, setHovered] = React.useState(false)\n  const [focused, setFocused] = React.useState(false)\n  const reduced = useReducedMotion()\n  const id = React.useId()\n  const open = hovered || focused\n\n  const close = () => {\n    setHovered(false)\n    setFocused(false)\n  }\n\n  // aria-describedby has to land on the element that actually takes focus (the\n  // ARIA tooltip pattern); on the wrapper span a screen reader announces\n  // nothing — hence injecting it via clone\n  const trigger = React.cloneElement(children as React.ReactElement<Record<string, unknown>>, {\n    \"aria-describedby\": open ? id : undefined,\n  })\n\n  return (\n    <span\n      className=\"relative inline-flex\"\n      onBlur={() => setFocused(false)}\n      onFocus={() => setFocused(true)}\n      onKeyDown={e => {\n        if (e.key === \"Escape\") close()\n      }}\n      onMouseEnter={() => setHovered(true)}\n      onMouseLeave={() => setHovered(false)}\n    >\n      {trigger}\n      <AnimatePresence>\n        {open && (\n          <motion.span\n            animate={{\n              opacity: 1,\n              y: 0,\n              scale: 1,\n              transition: reduced ? { duration: 0.15 } : { type: \"spring\", stiffness: 300, damping: 18 },\n            }}\n            className={cn(\n              \"pointer-events-none absolute left-1/2 z-50 -translate-x-1/2 whitespace-nowrap rounded-md bg-foreground px-2.5 py-1 text-xs text-background shadow-md\",\n              side === \"top\" ? \"bottom-full mb-2\" : \"top-full mt-2\",\n              className,\n            )}\n            exit={{ opacity: 0, transition: { duration: 0.15 } }}\n            id={id}\n            initial={reduced ? { opacity: 0 } : { opacity: 0, y: side === \"top\" ? 8 : -8, scale: 0.9 }}\n            role=\"tooltip\"\n          >\n            {content}\n            <span\n              aria-hidden=\"true\"\n              className={cn(\n                \"absolute left-1/2 size-2 -translate-x-1/2 rotate-45 bg-foreground\",\n                side === \"top\" ? \"top-full -mt-1\" : \"bottom-full -mb-1\",\n              )}\n            />\n          </motion.span>\n        )}\n      </AnimatePresence>\n    </span>\n  )\n}\n\nexport default AnimatedTooltip\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}