{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "liquid-cursor",
  "type": "registry:component",
  "title": "Liquid Cursor",
  "description": "Inside its card the pointer becomes a drop of liquid with a short trail; hover a link or button and the drop pours itself into that shape, then pinches free when you move on.",
  "categories": [
    "motion",
    "Liquid"
  ],
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/devl/liquid-cursor.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { motion, useMotionValue, useReducedMotion, useSpring, useTransform } from \"motion/react\";\nimport { type ComponentProps, type FocusEvent, type PointerEvent, useEffect, useId, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nfunction useReduce() {\n  const reduce = useReducedMotion();\n  const [mounted, setMounted] = useState(false);\n  useEffect(() => setMounted(true), []);\n  return mounted && !!reduce;\n}\n\nexport const liquidTarget = { \"data-liquid-target\": \"\" } as const;\n\nconst HEAD = 22;\nconst TAIL = [\n  { size: 19, rate: 30, gap: 9 },\n  { size: 16, rate: 26, gap: 8 },\n  { size: 13, rate: 22, gap: 7 },\n  { size: 10, rate: 18, gap: 6 },\n];\nconst headSpring = { stiffness: 520, damping: 38, mass: 0.7 };\n\nexport interface LiquidCursorProps extends ComponentProps<\"div\"> {\n  goo?: number;\n}\n\nexport function LiquidCursor({ goo = 6, className, children, ...props }: LiquidCursorProps) {\n  const filterId = `liquid-cursor-${useId().replace(/:/g, \"\")}`;\n  const reduce = useReduce();\n  const ref = useRef<HTMLDivElement>(null);\n  const tailRefs = useRef<(HTMLSpanElement | null)[]>([]);\n  const activeEl = useRef<HTMLElement | null>(null);\n  const pointer = useRef({ x: 0, y: 0 });\n  const tail = useRef(TAIL.map(() => ({ x: 0, y: 0 })));\n  const raf = useRef(0);\n  const [visible, setVisibleState] = useState(false);\n  const [mouse, setMouseState] = useState(false);\n  const live = useRef({ visible: false, mouse: false });\n  const setVisible = (v: boolean) => {\n    live.current.visible = v;\n    setVisibleState(v);\n  };\n  const setMouse = (v: boolean) => {\n    live.current.mouse = v;\n    setMouseState(v);\n  };\n\n  const tx = useMotionValue(0);\n  const ty = useMotionValue(0);\n  const tw = useMotionValue(HEAD);\n  const th = useMotionValue(HEAD);\n  const tr = useMotionValue(HEAD / 2);\n  const press = useMotionValue(1);\n  const cx = useSpring(tx, headSpring);\n  const cy = useSpring(ty, headSpring);\n  const w = useSpring(tw, headSpring);\n  const h = useSpring(th, headSpring);\n  const r = useSpring(tr, headSpring);\n  const scale = useSpring(press, { stiffness: 700, damping: 30 });\n  const left = useTransform(() => cx.get() - w.get() / 2);\n  const top = useTransform(() => cy.get() - h.get() / 2);\n\n  const paint = () => {\n    tail.current.forEach((p, i) => {\n      const el = tailRefs.current[i];\n      if (el) el.style.transform = `translate(${p.x}px, ${p.y}px)`;\n    });\n  };\n\n  const run = () => {\n    if (raf.current) return;\n    let last = performance.now();\n    const tick = (now: number) => {\n      const dt = Math.min((now - last) / 1000, 1 / 30);\n      last = now;\n      let moving = false;\n      let lead = { x: cx.get(), y: cy.get() };\n      tail.current.forEach((p, i) => {\n        const { rate, gap } = TAIL[i];\n        const k = 1 - Math.exp(-rate * dt);\n        p.x += (lead.x - p.x) * k;\n        p.y += (lead.y - p.y) * k;\n        const dx = p.x - lead.x;\n        const dy = p.y - lead.y;\n        const d = Math.hypot(dx, dy);\n        if (d > gap) {\n          p.x = lead.x + (dx / d) * gap;\n          p.y = lead.y + (dy / d) * gap;\n        }\n        if (d > 0.1) moving = true;\n        lead = p;\n      });\n      paint();\n      raf.current = moving ? requestAnimationFrame(tick) : 0;\n    };\n    raf.current = requestAnimationFrame(tick);\n  };\n\n  useEffect(() => () => cancelAnimationFrame(raf.current), []);\n\n  const setActive = (el: HTMLElement | null) => {\n    if (activeEl.current === el) return;\n    activeEl.current?.removeAttribute(\"data-liquid-active\");\n    el?.setAttribute(\"data-liquid-active\", \"\");\n    activeEl.current = el;\n  };\n\n  useEffect(() => () => setActive(null), []);\n\n  const local = (x: number, y: number) => {\n    const a = ref.current?.getBoundingClientRect();\n    return a ? { x: x - a.left, y: y - a.top } : { x: 0, y: 0 };\n  };\n\n  const shapeTo = (el: HTMLElement | null) => {\n    if (el) {\n      const b = el.getBoundingClientRect();\n      const c = local(b.left + b.width / 2, b.top + b.height / 2);\n      tx.set(c.x);\n      ty.set(c.y);\n      tw.set(b.width);\n      th.set(b.height);\n      tr.set(Math.min(Number.parseFloat(getComputedStyle(el).borderTopLeftRadius) || 0, b.height / 2));\n    } else {\n      tx.set(pointer.current.x);\n      ty.set(pointer.current.y);\n      tw.set(HEAD);\n      th.set(HEAD);\n      tr.set(HEAD / 2);\n    }\n    setActive(el);\n  };\n\n  const placeAll = (x: number, y: number) => {\n    pointer.current = { x, y };\n    for (const p of tail.current) Object.assign(p, { x, y });\n    paint();\n    tx.jump(x);\n    ty.jump(y);\n    cx.jump(x);\n    cy.jump(y);\n  };\n\n  const targetOf = (node: EventTarget | null) => {\n    const el = node instanceof Element ? node.closest<HTMLElement>(\"[data-liquid-target]\") : null;\n    return el && ref.current?.contains(el) ? el : null;\n  };\n\n  const enter = (event: PointerEvent<HTMLDivElement>) => {\n    props.onPointerEnter?.(event);\n    if (reduce || event.pointerType !== \"mouse\") return;\n    const p = local(event.clientX, event.clientY);\n    placeAll(p.x, p.y);\n    setMouse(true);\n    setVisible(true);\n  };\n\n  const move = (event: PointerEvent<HTMLDivElement>) => {\n    props.onPointerMove?.(event);\n    if (reduce || event.pointerType !== \"mouse\") return;\n    pointer.current = local(event.clientX, event.clientY);\n    shapeTo(targetOf(event.target));\n    run();\n  };\n\n  const leave = (event: PointerEvent<HTMLDivElement>) => {\n    props.onPointerLeave?.(event);\n    setMouse(false);\n    setVisible(false);\n    setActive(null);\n  };\n\n  const focus = (event: FocusEvent<HTMLDivElement>) => {\n    props.onFocus?.(event);\n    const el = targetOf(event.target);\n    if (reduce || live.current.mouse || !el || !el.matches(\":focus-visible\")) return;\n    const b = el.getBoundingClientRect();\n    const c = local(b.left + b.width / 2, b.top + b.height / 2);\n    if (live.current.visible) pointer.current = c;\n    else placeAll(c.x, c.y);\n    shapeTo(el);\n    run();\n    setVisible(true);\n  };\n\n  const blur = (event: FocusEvent<HTMLDivElement>) => {\n    props.onBlur?.(event);\n    if (live.current.mouse || ref.current?.contains(event.relatedTarget as Node | null)) return;\n    setVisible(false);\n    setActive(null);\n  };\n\n  return (\n    <div\n      {...props}\n      className={cn(\"relative isolate\", mouse && !reduce && \"cursor-none [&_*]:cursor-none\", className)}\n      onBlur={blur}\n      onFocus={focus}\n      onPointerDown={(e) => {\n        props.onPointerDown?.(e);\n        if (e.pointerType === \"mouse\") press.set(0.92);\n      }}\n      onPointerEnter={enter}\n      onPointerLeave={leave}\n      onPointerMove={move}\n      onPointerUp={(e) => {\n        props.onPointerUp?.(e);\n        press.set(1);\n      }}\n      ref={ref}\n    >\n      {!reduce && (\n        <motion.div\n          animate={{ opacity: visible ? 1 : 0 }}\n          aria-hidden\n          className=\"pointer-events-none absolute inset-0 -z-10 overflow-hidden rounded-[inherit]\"\n          initial={false}\n          transition={{ duration: visible ? 0.12 : 0.2, ease: \"easeOut\" }}\n        >\n          <svg className=\"absolute size-0\">\n            <filter colorInterpolationFilters=\"sRGB\" height=\"100%\" id={filterId} width=\"100%\" x=\"0\" y=\"0\">\n              <feGaussianBlur in=\"SourceGraphic\" result=\"blur\" stdDeviation={goo} />\n              <feColorMatrix in=\"blur\" values=\"1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 22 -9\" />\n            </filter>\n          </svg>\n          <div className=\"absolute inset-0\" style={{ filter: `url(#${filterId})` }}>\n            {TAIL.map((t, i) => (\n              <span\n                className=\"absolute top-0 left-0 rounded-full bg-brand\"\n                key={t.size}\n                ref={(el) => {\n                  tailRefs.current[i] = el;\n                }}\n                style={{ width: t.size, height: t.size, marginLeft: -t.size / 2, marginTop: -t.size / 2 }}\n              />\n            ))}\n            <motion.span\n              className=\"absolute top-0 left-0 bg-brand\"\n              style={{ x: left, y: top, width: w, height: h, borderRadius: r, scale }}\n            />\n          </div>\n        </motion.div>\n      )}\n      {children}\n    </div>\n  );\n}\n"
    }
  ]
}