{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "animated-tooltip",
  "type": "registry:component",
  "title": "Animated tooltip",
  "description": "A row of overlapping avatars where hovering or focusing one pops up a name and role card that leans and slides with the pointer.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/premium/animated-tooltip.tsx",
      "type": "registry:ui",
      "target": "components/ui/animated-tooltip.tsx",
      "content": "\"use client\"\nimport * as React from \"react\"\nimport { AnimatePresence, motion, useMotionValue, useReducedMotion, useSpring, useTransform } from \"motion/react\"\nimport { cn } from \"@/lib/utils\"\n\n/**\n * A row of overlapping avatars where hovering or focusing one pops up a name and role card that leans and slides with the pointer.\n * The card rises on a spring, tilts a few degrees toward the pointer as it\n * crosses the face, and settles back when it leaves. Keyboard focus opens\n * the same card centred; Escape closes it. Reduced motion shows and hides\n * the card instantly with no tilt.\n */\ntype AnimatedTooltipPerson = {\n  name: string\n  role: string\n  /** Avatar image. Initials are shown without one. */\n  src?: string\n  /** Override the initials derived from the name. */\n  initials?: string\n}\n\ntype AnimatedTooltipProps = {\n  people?: AnimatedTooltipPerson[]\n  /** Avatar diameter in pixels. */\n  size?: number\n  /** Index of a card to show before any hover. Defaults to the second person for the built-in sample only. */\n  defaultActive?: number\n  /** Called when an avatar is clicked or activated with the keyboard. */\n  onSelect?: (person: AnimatedTooltipPerson, index: number) => void\n  className?: string\n}\n\nconst TEAM: AnimatedTooltipPerson[] = [\n  { name: \"Maya Chen\", role: \"Founder, Lumen\" },\n  { name: \"Theo Okafor\", role: \"Head of Engineering\" },\n  { name: \"Priya Raman\", role: \"Product Designer\" },\n  { name: \"Sam Lowe\", role: \"Finance Lead\" },\n  { name: \"Noor Haddad\", role: \"Customer Success\" },\n]\n\nconst TINTS = [\"bg-accent-soft text-accent-fg\", \"bg-ink text-on-ink\", \"bg-sunken text-fg\", \"bg-accent text-on-accent\", \"bg-surface text-fg-muted border border-border\"]\n\nconst SNAPPY = { type: \"spring\", bounce: 0.18, duration: 0.42 } as const\nconst LEAN = { stiffness: 300, damping: 24, mass: 0.7 }\n\nfunction initialsOf(name: string) {\n  return name\n    .split(/\\s+/)\n    .filter(Boolean)\n    .slice(0, 2)\n    .map((w) => w[0]!.toUpperCase())\n    .join(\"\")\n}\n\nfunction AnimatedTooltip({ people, size = 44, defaultActive, onSelect, className }: AnimatedTooltipProps) {\n  const reduce = useReducedMotion()\n  const list = people ?? TEAM\n  const id = React.useId()\n  const [active, setActive] = React.useState<number | null>(defaultActive ?? (people === undefined ? 1 : null))\n  // -0.5 at the left edge of the face, 0.5 at the right. One set of values serves every card, since only one is open.\n  const x = useMotionValue(0)\n  const rotate = useSpring(useTransform(x, [-0.5, 0.5], [-9, 9]), LEAN)\n  const shift = useSpring(useTransform(x, [-0.5, 0.5], [-12, 12]), LEAN)\n\n  return (\n    <div data-slot=\"animated-tooltip\" data-tier=\"premium\" className={cn(\"mx-auto flex w-fit items-center\", className)}>\n      {list.map((p, i) => {\n        const open = active === i\n        const tipId = `${id}-${i}`\n        return (\n          <span key={`${p.name}-${i}`} className=\"relative\" style={{ marginLeft: i ? -Math.round(size * 0.22) : 0, zIndex: open ? 20 : undefined }}>\n            <span className=\"pointer-events-none absolute bottom-full left-1/2 z-30 mb-2.5 -translate-x-1/2\">\n              <AnimatePresence initial={false}>\n                {open ? (\n                  <motion.span\n                    key=\"card\"\n                    id={tipId}\n                    role=\"tooltip\"\n                    data-slot=\"animated-tooltip-card\"\n                    initial={reduce ? { opacity: 0 } : { opacity: 0, y: 10, scale: 0.85 }}\n                    animate={{ opacity: 1, y: 0, scale: 1 }}\n                    exit={reduce ? { opacity: 0 } : { opacity: 0, y: 6, scale: 0.9 }}\n                    transition={reduce ? { duration: 0 } : SNAPPY}\n                    style={reduce ? undefined : { rotate, x: shift }}\n                    className=\"relative block origin-bottom rounded-lg bg-ink px-3 py-1.5 text-left whitespace-nowrap shadow-overlay\"\n                  >\n                    <span className=\"block text-xs font-medium text-on-ink\">{p.name}</span>\n                    <span className=\"block text-[11px] text-on-ink/70\">{p.role}</span>\n                    <span aria-hidden className=\"absolute top-full left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 rotate-45 rounded-[1px] bg-ink\" />\n                  </motion.span>\n                ) : null}\n              </AnimatePresence>\n            </span>\n            <motion.button\n              type=\"button\"\n              aria-label={`${p.name}, ${p.role}`}\n              aria-describedby={open ? tipId : undefined}\n              data-slot=\"animated-tooltip-avatar\"\n              data-state={open ? \"open\" : \"closed\"}\n              onPointerEnter={(e) => {\n                if (e.pointerType === \"mouse\") setActive(i)\n              }}\n              onPointerMove={(e) => {\n                if (reduce || e.pointerType !== \"mouse\") return\n                const r = e.currentTarget.getBoundingClientRect()\n                x.set(Math.max(-0.5, Math.min(0.5, (e.clientX - r.left) / r.width - 0.5)))\n              }}\n              onPointerLeave={(e) => {\n                if (e.pointerType !== \"mouse\") return\n                x.set(0)\n                setActive((a) => (a === i ? null : a))\n              }}\n              onFocus={() => {\n                x.set(0)\n                setActive(i)\n              }}\n              onBlur={() => setActive((a) => (a === i ? null : a))}\n              onKeyDown={(e) => {\n                if (e.key === \"Escape\") setActive(null)\n              }}\n              onClick={() => {\n                setActive((a) => (a === i ? null : i))\n                onSelect?.(p, i)\n              }}\n              whileHover={reduce ? undefined : { scale: 1.1 }}\n              transition={reduce ? { duration: 0 } : SNAPPY}\n              className={cn(\n                \"relative grid shrink-0 cursor-pointer place-items-center overflow-hidden rounded-full font-medium ring-2 ring-bg outline-none select-none\",\n                \"focus-visible:ring-accent\",\n                !p.src && TINTS[i % TINTS.length]\n              )}\n              style={{ width: size, height: size, fontSize: Math.round(size * 0.32) }}\n            >\n              {p.src ? (\n                <img src={p.src} alt=\"\" className=\"size-full object-cover\" />\n              ) : (\n                <span aria-hidden>{p.initials ?? initialsOf(p.name)}</span>\n              )}\n            </motion.button>\n          </span>\n        )\n      })}\n    </div>\n  )\n}\n\nexport { AnimatedTooltip }\nexport type { AnimatedTooltipProps, AnimatedTooltipPerson }\n"
    }
  ]
}
