{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"cursor-highlight","type":"registry:ui","title":"Cursor Highlight","description":"A soft glow follows the cursor and springs into a pill that wraps whatever link or card you hover.","dependencies":[],"registryDependencies":["utils"],"files":[{"path":"registry/ui/cursor-highlight.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useEffect, useRef, type ReactNode } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface CursorHighlightProps {\n  children: ReactNode;\n  /** Elements the highlight snaps to. */\n  selector?: string;\n  color?: string;\n  /** Peak opacity of the highlight, 0–1. */\n  opacity?: number;\n  /** Diameter of the free-floating blob in px. */\n  blobSize?: number;\n  /** Px the highlight extends past a hovered element. */\n  padding?: number;\n  /** Corner radius when wrapped around an element, in px. */\n  radius?: number;\n  /** Spring stiffness, 0–1. Higher = snappier. */\n  stiffness?: number;\n  className?: string;\n}\n\nconst KEYS = [\"x\", \"y\", \"w\", \"h\", \"r\", \"blur\", \"a\"] as const;\ntype State = Record<(typeof KEYS)[number], number>;\n\nexport function CursorHighlight({\n  children,\n  selector = \"[data-highlight], a, button\",\n  color = \"#c6ff3d\",\n  opacity = 0.16,\n  blobSize = 90,\n  padding = 4,\n  radius = 12,\n  stiffness = 0.2,\n  className,\n}: CursorHighlightProps) {\n  const rootRef = useRef<HTMLDivElement>(null);\n  const blobRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    const root = rootRef.current!;\n    const blob = blobRef.current!;\n    const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n    const cur: State = { x: 0, y: 0, w: blobSize, h: blobSize, r: blobSize / 2, blur: 0, a: 0 };\n    const vel: State = { x: 0, y: 0, w: 0, h: 0, r: 0, blur: 0, a: 0 };\n    const tgt: State = { ...cur };\n    const pointer = { x: 0, y: 0, in: false };\n    let attached: Element | null = null;\n    let raf = 0;\n\n    const aim = () => {\n      if (attached) {\n        const rr = root.getBoundingClientRect();\n        const b = attached.getBoundingClientRect();\n        Object.assign(tgt, {\n          x: b.left - rr.left - padding,\n          y: b.top - rr.top - padding,\n          w: b.width + padding * 2,\n          h: b.height + padding * 2,\n          r: radius,\n          blur: 0,\n          a: 1,\n        });\n      } else {\n        Object.assign(tgt, {\n          x: pointer.x - blobSize / 2,\n          y: pointer.y - blobSize / 2,\n          w: blobSize,\n          h: blobSize,\n          r: blobSize / 2,\n          blur: blobSize * 0.3,\n          a: pointer.in ? 0.7 : 0,\n        });\n      }\n    };\n\n    const loop = () => {\n      aim();\n      let moving = false;\n      for (const k of KEYS) {\n        if (reduced) cur[k] = tgt[k];\n        else {\n          // Slightly underdamped spring: a touch of overshoot sells the morph.\n          vel[k] = vel[k] * 0.7 + (tgt[k] - cur[k]) * stiffness;\n          cur[k] += vel[k];\n        }\n        if (Math.abs(tgt[k] - cur[k]) > 0.05 || Math.abs(vel[k]) > 0.05) moving = true;\n      }\n      blob.style.transform = `translate3d(${cur.x}px, ${cur.y}px, 0)`;\n      blob.style.width = `${Math.max(0, cur.w)}px`;\n      blob.style.height = `${Math.max(0, cur.h)}px`;\n      blob.style.borderRadius = `${Math.max(0, cur.r)}px`;\n      blob.style.filter = `blur(${Math.max(0, cur.blur)}px)`;\n      blob.style.opacity = String(Math.max(0, Math.min(1, cur.a)));\n      raf = moving ? requestAnimationFrame(loop) : 0;\n    };\n    const wake = () => {\n      if (!raf) raf = requestAnimationFrame(loop);\n    };\n    const snap = (next: Element | null) => {\n      // Jump straight to the first target instead of flying in from a stale spot.\n      if (next && cur.a < 0.05) {\n        attached = next;\n        aim();\n        Object.assign(cur, tgt, { a: 0 });\n      }\n      attached = next;\n      wake();\n    };\n\n    const onMove = (e: PointerEvent) => {\n      const r = root.getBoundingClientRect();\n      pointer.x = e.clientX - r.left;\n      pointer.y = e.clientY - r.top;\n      if (!pointer.in) {\n        pointer.in = true;\n        if (cur.a < 0.05) Object.assign(cur, { x: pointer.x - blobSize / 2, y: pointer.y - blobSize / 2 });\n      }\n      const hit = (e.target as Element).closest?.(selector);\n      snap(hit && root.contains(hit) ? hit : null);\n    };\n    const onLeave = () => {\n      pointer.in = false;\n      attached = null;\n      wake();\n    };\n    const onFocus = (e: FocusEvent) => {\n      const hit = (e.target as Element).closest?.(selector);\n      if (hit && root.contains(hit)) snap(hit);\n    };\n    const onBlur = () => {\n      if (!pointer.in) {\n        attached = null;\n        wake();\n      }\n    };\n\n    root.addEventListener(\"pointermove\", onMove, { passive: true });\n    root.addEventListener(\"pointerleave\", onLeave);\n    root.addEventListener(\"focusin\", onFocus);\n    root.addEventListener(\"focusout\", onBlur);\n    return () => {\n      cancelAnimationFrame(raf);\n      root.removeEventListener(\"pointermove\", onMove);\n      root.removeEventListener(\"pointerleave\", onLeave);\n      root.removeEventListener(\"focusin\", onFocus);\n      root.removeEventListener(\"focusout\", onBlur);\n    };\n  }, [selector, blobSize, padding, radius, stiffness]);\n\n  return (\n    <div ref={rootRef} className={cn(\"relative isolate\", className)}>\n      <div\n        ref={blobRef}\n        aria-hidden\n        style={{\n          position: \"absolute\",\n          left: 0,\n          top: 0,\n          zIndex: -1,\n          opacity: 0,\n          pointerEvents: \"none\",\n          willChange: \"transform, width, height\",\n          background: `color-mix(in srgb, ${color} ${opacity * 100}%, transparent)`,\n          boxShadow: `0 0 0 1px color-mix(in srgb, ${color} ${Math.min(100, opacity * 150)}%, transparent), 0 8px 32px -8px color-mix(in srgb, ${color} ${Math.min(100, opacity * 200)}%, transparent)`,\n        }}\n      />\n      {children}\n    </div>\n  );\n}\n"}]}