{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "smooth-cursor",
  "title": "Smooth Cursor",
  "description": "Dot-and-ring custom cursor; the ring trails on a spring and grows over links and buttons.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/components/velora/smooth-cursor.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef, useState, useSyncExternalStore } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { motion, useMotionValue, useReducedMotion, useSpring } from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst FINE = \"(pointer: fine)\";\nconst subscribeFine = (onChange: () => void) => {\n  const query = matchMedia(FINE);\n  query.addEventListener(\"change\", onChange);\n  return () => query.removeEventListener(\"change\", onChange);\n};\nconst SPRING = { stiffness: 300, damping: 28, mass: 0.6 };\n\ninterface SmoothCursorProps {\n  /** Limit the cursor to this element; defaults to the whole document */\n  container?: React.RefObject<HTMLElement | null>;\n  /** Hide the system cursor while the custom one is showing */\n  hideNativeCursor?: boolean;\n  /** CSS selector for elements that make the ring grow */\n  interactiveSelector?: string;\n  /** Classes for the dot */\n  dotClassName?: string;\n  /** Classes for the trailing ring */\n  ringClassName?: string;\n}\n\n/**\n * A page-level custom cursor: a dot that sits on the pointer and a ring that\n * trails it on a spring and grows over links, buttons and form controls.\n * Mount it once (e.g. in a layout). It only runs for a mouse on a\n * fine-pointer device, hides while the pointer is outside the window or\n * container, and never receives pointer events.\n */\nexport function SmoothCursor({\n  container,\n  hideNativeCursor = true,\n  interactiveSelector = \"a, button, [role=button], input, select, textarea, label, summary\",\n  dotClassName,\n  ringClassName,\n}: SmoothCursorProps) {\n  // false on the server and on touch screens, so nothing renders there.\n  const fine = useSyncExternalStore(subscribeFine, () => matchMedia(FINE).matches, () => false);\n  const reducedMotion = useReducedMotion();\n  const [visible, setVisible] = useState(false);\n  const [hovering, setHovering] = useState(false);\n  const [pressed, setPressed] = useState(false);\n  const shown = useRef(false);\n\n  const x = useMotionValue(0);\n  const y = useMotionValue(0);\n  const ringX = useSpring(x, SPRING);\n  const ringY = useSpring(y, SPRING);\n\n  useEffect(() => {\n    if (!fine) return;\n    const root = container ? container.current : document.documentElement;\n    if (!root) return;\n\n    const hide = () => {\n      shown.current = false;\n      setVisible(false);\n      setPressed(false);\n    };\n    const move = (event: PointerEvent) => {\n      if (event.pointerType !== \"mouse\") return hide();\n      x.set(event.clientX);\n      y.set(event.clientY);\n      if (!shown.current) {\n        ringX.jump(event.clientX);\n        ringY.jump(event.clientY);\n        shown.current = true;\n        setVisible(true);\n      }\n      const target = event.target as Element | null;\n      setHovering(!!target?.closest?.(interactiveSelector));\n    };\n    const down = () => setPressed(true);\n    const up = () => setPressed(false);\n    // relatedTarget is null when the pointer leaves the window.\n    const out = (event: PointerEvent) => !event.relatedTarget && hide();\n\n    root.addEventListener(\"pointermove\", move, { passive: true });\n    root.addEventListener(\"pointerdown\", down);\n    root.addEventListener(\"pointerleave\", hide);\n    addEventListener(\"pointerup\", up);\n    addEventListener(\"blur\", hide);\n    document.addEventListener(\"pointerout\", out);\n    return () => {\n      root.removeEventListener(\"pointermove\", move);\n      root.removeEventListener(\"pointerdown\", down);\n      root.removeEventListener(\"pointerleave\", hide);\n      removeEventListener(\"pointerup\", up);\n      removeEventListener(\"blur\", hide);\n      document.removeEventListener(\"pointerout\", out);\n      hide();\n    };\n  }, [fine, container, interactiveSelector, x, y, ringX, ringY]);\n\n  // Hide the system cursor only while ours is on screen; restored on unmount.\n  useEffect(() => {\n    const root = container ? container.current : document.documentElement;\n    if (!root || !visible || !hideNativeCursor) return;\n    root.setAttribute(\"data-smooth-cursor\", \"\");\n    return () => root.removeAttribute(\"data-smooth-cursor\");\n  }, [visible, hideNativeCursor, container]);\n\n  if (!fine) return null;\n\n  const snap = reducedMotion ? { duration: 0 } : { duration: 0.2 };\n  const scale = (hovering ? 1.8 : 1) * (pressed ? 0.8 : 1);\n\n  return createPortal(\n    <div\n      aria-hidden\n      data-slot=\"smooth-cursor\"\n      className=\"pointer-events-none fixed top-0 left-0 z-[9999]\"\n    >\n      <style>{\"[data-smooth-cursor],[data-smooth-cursor] *{cursor:none!important}\"}</style>\n      <motion.div\n        style={reducedMotion ? { x, y } : { x: ringX, y: ringY }}\n        className=\"absolute top-0 left-0\"\n      >\n        <motion.div\n          initial={false}\n          animate={{ scale, opacity: visible ? 1 : 0 }}\n          transition={snap}\n          className={cn(\n            \"-mt-4 -ml-4 size-8 rounded-full border-[1.5px] border-primary/60 transition-colors motion-reduce:transition-none\",\n            hovering && \"bg-primary/10\",\n            ringClassName\n          )}\n        />\n      </motion.div>\n      <motion.div\n        style={{ x, y }}\n        initial={false}\n        animate={{ opacity: visible ? 1 : 0, scale: hovering ? 0.5 : 1 }}\n        transition={snap}\n        className={cn(\"absolute -top-1 -left-1 size-2 rounded-full bg-primary\", dotClassName)}\n      />\n    </div>,\n    document.body\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/velora/smooth-cursor.tsx"
    }
  ],
  "type": "registry:ui"
}