{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"lean-tip","type":"registry:ui","title":"Lean Tip","description":"A tooltip that springs out of its trigger with a soft bounce, then drifts and leans after the pointer on a warm glow. Opens on focus, closes on Escape.","dependencies":["motion"],"registryDependencies":[],"files":[{"path":"registry/ui/lean-tip.tsx","type":"registry:ui","content":"\"use client\";\n\nimport {\n  cloneElement,\n  isValidElement,\n  useEffect,\n  useId,\n  useRef,\n  useState,\n  type PointerEvent,\n  type ReactElement,\n  type ReactNode,\n} from \"react\";\nimport { AnimatePresence, motion, useMotionValue, useReducedMotion, useSpring, useTransform } from \"motion/react\";\n\nexport interface LeanTipProps {\n  content: ReactNode;\n  /** The trigger: one focusable element (button, link...). */\n  children: ReactElement<{ \"aria-describedby\"?: string }>;\n  side?: \"top\" | \"bottom\";\n  /** Ms before the tooltip opens on hover or focus. */\n  delay?: number;\n  /** How far the bubble drifts toward the pointer, 0-1. */\n  follow?: number;\n  /** Warm glow under the bubble. */\n  glow?: string;\n  className?: string;\n}\n\nexport function LeanTip({\n  content,\n  children,\n  side = \"top\",\n  delay = 150,\n  follow = 0.3,\n  glow = \"#fb923c\",\n  className,\n}: LeanTipProps) {\n  const id = useId();\n  const reduce = useReducedMotion();\n  const [open, setOpen] = useState(false);\n  const timer = useRef<number | undefined>(undefined);\n  const wrap = useRef<HTMLSpanElement>(null);\n\n  const mx = useMotionValue(0);\n  const x = useSpring(mx, { stiffness: 260, damping: 18 });\n  // Follow-through: the bubble leans against the direction it is being pulled.\n  const rotate = useTransform(() => Math.max(-8, Math.min(8, (mx.get() - x.get()) * 0.25)));\n  const top = side === \"top\";\n\n  const show = () => {\n    window.clearTimeout(timer.current);\n    timer.current = window.setTimeout(() => setOpen(true), delay);\n  };\n  const hide = () => {\n    window.clearTimeout(timer.current);\n    setOpen(false);\n    mx.set(0);\n  };\n  useEffect(() => () => window.clearTimeout(timer.current), []);\n\n  const onMove = (e: PointerEvent) => {\n    const r = wrap.current?.getBoundingClientRect();\n    if (r && !reduce) mx.set((e.clientX - (r.left + r.width / 2)) * follow);\n  };\n\n  return (\n    <span\n      ref={wrap}\n      className=\"relative inline-flex\"\n      onPointerEnter={show}\n      onPointerMove={onMove}\n      onPointerLeave={hide}\n      onFocus={show}\n      onBlur={hide}\n      onKeyDown={(e) => e.key === \"Escape\" && hide()}\n    >\n      {isValidElement(children) ? cloneElement(children, { \"aria-describedby\": open ? id : undefined }) : children}\n      <AnimatePresence>\n        {open && (\n          <span\n            className={`pointer-events-none absolute left-1/2 z-50 -translate-x-1/2 ${top ? \"bottom-full pb-2.5\" : \"top-full pt-2.5\"}`}\n          >\n            <motion.span\n              id={id}\n              role=\"tooltip\"\n              className={`relative block w-max max-w-64 rounded-xl bg-foreground px-3 py-1.5 text-sm font-medium text-background ${className ?? \"\"}`}\n              style={{\n                x,\n                rotate,\n                transformOrigin: top ? \"50% 100%\" : \"50% 0%\",\n                boxShadow: `0 10px 28px -8px ${glow}, 0 0 0 1px color-mix(in oklab, ${glow} 35%, transparent)`,\n              }}\n              initial={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.4, y: top ? 10 : -10 }}\n              animate={{ opacity: 1, scale: 1, y: 0 }}\n              exit={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.7, y: top ? 6 : -6, transition: { duration: 0.12 } }}\n              transition={{ type: \"spring\", stiffness: 520, damping: 14, mass: 0.7 }}\n            >\n              {content}\n              <span\n                aria-hidden\n                className={`absolute left-1/2 size-2.5 -translate-x-1/2 rotate-45 rounded-[2px] bg-foreground ${top ? \"-bottom-1\" : \"-top-1\"}`}\n              />\n            </motion.span>\n          </span>\n        )}\n      </AnimatePresence>\n    </span>\n  );\n}\n"}]}