{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "smooth-cursor",
  "title": "Smooth Cursor",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A custom cursor for one area that glides after the pointer on a spring, leans into the direction of travel, shrinks on press and grows a label over elements marked with data-cursor-label.",
  "dependencies": [
    "motion@^12"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/smooth-cursor.tsx",
      "content": "// Ballmac UI: Smooth Cursor. https://ui.ballmac.com/components/smooth-cursor\n\"use client\"\n\nimport * as React from \"react\"\nimport { AnimatePresence, motion, useMotionValue, useReducedMotion, useSpring } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\n\ntype SmoothCursorProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  /** The area the custom cursor lives in. Outside it, the normal cursor returns. */\n  children: React.ReactNode\n  /** Your own cursor graphic. The default is an arrow that leans into the direction you are moving. */\n  cursor?: React.ReactNode\n  /** Hide the system cursor inside the area. Text fields always keep theirs. */\n  hideNative?: boolean\n  /** Stiffness of the follow spring. Lower is floatier. */\n  stiffness?: number\n}\n\nconst DefaultArrow = () => (\n  <svg width=\"26\" height=\"26\" viewBox=\"0 0 26 26\" fill=\"none\" className=\"drop-shadow-[0_2px_4px_rgb(0_0_0/0.3)]\">\n    <path d=\"M4 3.2c0-.9 1-1.4 1.7-.8l17 14.2c.8.7.4 2-.7 2.1l-7.2.7-3.3 6.4c-.5.9-1.8.8-2.1-.2L4 3.2Z\" className=\"fill-foreground stroke-background\" strokeWidth=\"1.6\" strokeLinejoin=\"round\" />\n  </svg>\n)\n\n/**\n * Replaces the pointer with a cursor that glides after the real one. Add data-cursor-label=\"View\" to any\n * element and the cursor grows a label while it is over it. Mouse and pen only; touch is left alone.\n */\nfunction SmoothCursor({ children, cursor, hideNative = true, stiffness = 420, className, onPointerMove, onPointerLeave, onPointerDown, onPointerUp, ...props }: SmoothCursorProps) {\n  const reduce = useReducedMotion()\n  const x = useMotionValue(-100)\n  const y = useMotionValue(-100)\n  const sx = useSpring(x, { stiffness, damping: 34, mass: 0.6 })\n  const sy = useSpring(y, { stiffness, damping: 34, mass: 0.6 })\n  const turn = useSpring(0, { stiffness: 200, damping: 22 })\n  const [visible, setVisible] = React.useState(false)\n  const [pressed, setPressed] = React.useState(false)\n  const [label, setLabel] = React.useState<string | null>(null)\n  const [overField, setOverField] = React.useState(false)\n  const last = React.useRef<{ x: number; y: number } | null>(null)\n  const settle = React.useRef(0)\n  React.useEffect(() => () => window.clearTimeout(settle.current), [])\n\n  return (\n    <div\n      data-slot=\"smooth-cursor\"\n      onPointerMove={(e) => {\n        onPointerMove?.(e)\n        if (e.pointerType === \"touch\") return\n        const box = e.currentTarget.getBoundingClientRect()\n        const px = e.clientX - box.left\n        const py = e.clientY - box.top\n        x.set(px)\n        y.set(py)\n        if (reduce) {\n          sx.jump(px)\n          sy.jump(py)\n        }\n        // Lean the arrow into the direction of travel, then let it settle upright when the pointer stops.\n        const prev = last.current\n        if (prev && !reduce) {\n          turn.set(Math.max(-28, Math.min(28, (px - prev.x) * 1.4)))\n          window.clearTimeout(settle.current)\n          settle.current = window.setTimeout(() => turn.set(0), 110)\n        }\n        last.current = { x: px, y: py }\n        const target = e.target as HTMLElement\n        setLabel(target.closest<HTMLElement>(\"[data-cursor-label]\")?.dataset.cursorLabel ?? null)\n        setOverField(!!target.closest(\"input, textarea, select, [contenteditable=true], [data-cursor=native]\"))\n        setVisible(true)\n      }}\n      onPointerLeave={(e) => {\n        onPointerLeave?.(e)\n        setVisible(false)\n        setPressed(false)\n        last.current = null\n      }}\n      onPointerDown={(e) => {\n        onPointerDown?.(e)\n        if (e.pointerType !== \"touch\") setPressed(true)\n      }}\n      onPointerUp={(e) => {\n        onPointerUp?.(e)\n        setPressed(false)\n      }}\n      className={cn(\n        \"relative\",\n        hideNative && \"[@media(pointer:fine)]:[&_*]:cursor-none [@media(pointer:fine)]:[&_input]:cursor-text [@media(pointer:fine)]:[&_textarea]:cursor-text [@media(pointer:fine)]:[&_[data-cursor=native]]:cursor-auto\",\n        className\n      )}\n      {...props}\n    >\n      {children}\n      <AnimatePresence>\n        {visible && !overField && (\n          <motion.div\n            aria-hidden=\"true\"\n            className=\"pointer-events-none absolute top-0 start-0 z-50 hidden [@media(pointer:fine)]:block\"\n            style={{ x: sx, y: sy }}\n            initial={{ opacity: 0, scale: 0.6 }}\n            animate={{ opacity: 1, scale: pressed ? 0.82 : label ? 1.1 : 1 }}\n            exit={{ opacity: 0, scale: 0.6 }}\n            transition={{ type: \"spring\", stiffness: 500, damping: 30 }}\n          >\n            <motion.div style={{ rotate: reduce ? 0 : turn, originX: 0.15, originY: 0.1 }}>{cursor ?? <DefaultArrow />}</motion.div>\n            {label && (\n              <motion.span\n                initial={{ opacity: 0, x: -4 }}\n                animate={{ opacity: 1, x: 0 }}\n                className=\"absolute top-5 start-6 rounded-full bg-foreground px-2.5 py-1 text-xs font-medium whitespace-nowrap text-background shadow-md\"\n              >\n                {label}\n              </motion.span>\n            )}\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </div>\n  )\n}\n\nexport { SmoothCursor, type SmoothCursorProps }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/smooth-cursor.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "cursor",
      "pointer",
      "spring",
      "hover",
      "label"
    ],
    "version": "1.0.0",
    "updated": "2026-10-01",
    "ai": {
      "summary": "<SmoothCursor cursor hideNative stiffness>area</SmoothCursor>. It only covers its own area, ignores touch, leaves text fields on the normal cursor and sets no global styles. data-cursor-label='View' on any child adds a label.",
      "whenToUse": [
        "Portfolios and galleries",
        "Product tours with hover storytelling"
      ],
      "whenNotToUse": [
        "Form-heavy or data-dense screens",
        "Touch-first products"
      ],
      "composesWith": [
        "magnetic-button",
        "tilt-card"
      ],
      "a11y": [
        {
          "keys": "Keyboard",
          "action": "Unaffected: focus rings and tab order do not change"
        },
        {
          "keys": "Screen readers",
          "action": "The cursor is aria-hidden"
        },
        {
          "keys": "Reduced motion",
          "action": "The cursor follows the pointer with no spring or lean"
        }
      ],
      "customization": [
        "cursor",
        "hideNative",
        "stiffness",
        "data-cursor-label",
        "data-cursor='native'"
      ]
    },
    "examples": [
      "smooth-cursor-demo",
      "smooth-cursor-custom"
    ],
    "url": "https://ui.ballmac.com/components/smooth-cursor"
  },
  "categories": [
    "motion"
  ],
  "type": "registry:ui"
}