{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "custom-cursor",
  "title": "Custom Cursor",
  "description": "A dot and a trailing ring that follow a mouse pointer, growing over interactive elements and showing labels.",
  "registryDependencies": [
    "@corsair-ui/utils",
    "@corsair-ui/use-media-query"
  ],
  "files": [
    {
      "path": "registry/default/ui/custom-cursor.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef, useState, type ComponentProps, type ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\n\nimport { useMediaQuery } from \"@/registry/default/hooks/use-media-query\";\nimport { cn } from \"@/registry/default/lib/utils\";\n\nconst INTERACTIVE =\n  \"a[href], button, [role=button], input, select, textarea, label, summary, [data-cursor]\";\n\nconst HIDE_NATIVE_CURSOR =\n  \"html[data-custom-cursor], html[data-custom-cursor] * { cursor: none !important; }\";\n\ntype CustomCursorState = \"default\" | \"hover\" | \"pressed\" | \"hidden\";\n\ninterface CustomCursorProps extends ComponentProps<\"div\"> {\n  /** Diameter of the ring at rest, in px. */\n  size?: number;\n  /** Diameter of the ring over something interactive, in px. */\n  hoverSize?: number;\n  /** Diameter of the dot that sits exactly on the pointer, in px. 0 leaves it out. */\n  dotSize?: number;\n  /** From 0 to 1: how much of the way to the pointer the ring catches up each frame. 1 sticks to it. */\n  smoothing?: number;\n  /** Invert what is underneath (`mix-blend-difference`), so it shows on light and dark alike. */\n  blend?: boolean;\n  /** Hide the system cursor while this one is shown. */\n  hideNativeCursor?: boolean;\n  /** What counts as interactive: the ring grows over it. */\n  interactiveSelector?: string;\n  /** Content shown inside the ring over an element with `data-cursor=\"<key>\"`. */\n  labels?: Record<string, ReactNode>;\n  /**\n   * Extra decoration drawn with the cursor, such as crosshair lines. The\n   * root sets `--cursor-x` and `--cursor-y` to the pointer's position in px\n   * on every move, and `data-state` for hover and press, so children can\n   * follow it with CSS alone.\n   */\n  children?: ReactNode;\n}\n\n/**\n * A cursor of your own: a dot that sits on the pointer and a ring that\n * trails it, growing over links and buttons and showing a label over\n * elements marked `data-cursor`. It only appears for a mouse or trackpad\n * (`(hover: hover) and (pointer: fine)`) without `prefers-reduced-motion`;\n * everyone else keeps the system cursor, untouched. It is decorative and\n * hidden from screen readers, never takes pointer events, and moves by\n * writing transforms once per frame, stopping when the pointer rests.\n * `data-state` is \"default\", \"hover\", \"pressed\", or \"hidden\" while the\n * pointer is outside the window. Children are drawn with it, and can follow\n * the pointer through `--cursor-x` and `--cursor-y`.\n *\n * @example\n * <CustomCursor labels={{ view: \"View\" }} />\n * <a href=\"/work/1\" data-cursor=\"view\">…</a>\n *\n * @example\n * // A crosshair: lines across the window that meet at the pointer.\n * <CustomCursor>\n *   <div className=\"fixed inset-y-0 left-[var(--cursor-x)] w-px bg-current opacity-30\" />\n *   <div className=\"fixed inset-x-0 top-[var(--cursor-y)] h-px bg-current opacity-30\" />\n * </CustomCursor>\n */\nfunction CustomCursor(props: CustomCursorProps) {\n  const fine = useMediaQuery(\"(hover: hover) and (pointer: fine)\");\n  const reduced = useMediaQuery(\"(prefers-reduced-motion: reduce)\");\n  if (!fine || reduced) return null;\n  return <CursorFollower {...props} />;\n}\n\nfunction CursorFollower({\n  size = 12,\n  hoverSize = 40,\n  dotSize = 4,\n  smoothing = 0.2,\n  blend = true,\n  hideNativeCursor = true,\n  interactiveSelector = INTERACTIVE,\n  labels,\n  className,\n  children,\n  ...props\n}: CustomCursorProps) {\n  const root = useRef<HTMLDivElement>(null);\n  const dot = useRef<HTMLDivElement>(null);\n  const ring = useRef<HTMLDivElement>(null);\n  const [visible, setVisible] = useState(false);\n  const [hovering, setHovering] = useState(false);\n  const [pressed, setPressed] = useState(false);\n  const [labelKey, setLabelKey] = useState<string | null>(null);\n\n  const settings = useRef({ smoothing, interactiveSelector });\n  useEffect(() => {\n    settings.current = { smoothing, interactiveSelector };\n  });\n\n  useEffect(() => {\n    if (!hideNativeCursor) return;\n    const html = document.documentElement;\n    html.setAttribute(\"data-custom-cursor\", \"\");\n    return () => html.removeAttribute(\"data-custom-cursor\");\n  }, [hideNativeCursor]);\n\n  useEffect(() => {\n    const target = { x: 0, y: 0 };\n    const trail = { x: 0, y: 0 };\n    let seen = false;\n    let frame = 0;\n\n    const place = () => {\n      // The exact pointer, for children that draw more than the dot and ring.\n      root.current?.style.setProperty(\"--cursor-x\", `${target.x}px`);\n      root.current?.style.setProperty(\"--cursor-y\", `${target.y}px`);\n      if (dot.current) {\n        dot.current.style.transform = `translate3d(${target.x}px, ${target.y}px, 0) translate(-50%, -50%)`;\n      }\n      if (ring.current) {\n        ring.current.style.transform = `translate3d(${trail.x}px, ${trail.y}px, 0) translate(-50%, -50%)`;\n      }\n    };\n    const tick = () => {\n      const factor = Math.min(Math.max(settings.current.smoothing, 0.01), 1);\n      trail.x += (target.x - trail.x) * factor;\n      trail.y += (target.y - trail.y) * factor;\n      const resting = Math.abs(target.x - trail.x) < 0.1 && Math.abs(target.y - trail.y) < 0.1;\n      if (resting) {\n        trail.x = target.x;\n        trail.y = target.y;\n      }\n      place();\n      frame = resting ? 0 : requestAnimationFrame(tick);\n    };\n\n    const onMove = (event: PointerEvent) => {\n      if (event.pointerType === \"touch\") {\n        setVisible(false);\n        return;\n      }\n      target.x = event.clientX;\n      target.y = event.clientY;\n      if (!seen) {\n        // Start on the pointer instead of flying in from the corner.\n        seen = true;\n        trail.x = target.x;\n        trail.y = target.y;\n      }\n      setVisible(true);\n      if (!frame) frame = requestAnimationFrame(tick);\n    };\n    const onOver = (event: PointerEvent) => {\n      const element = event.target instanceof Element ? event.target : null;\n      const interactive = element?.closest(settings.current.interactiveSelector);\n      // A data-cursor on <html> or <body> sets a page-wide label; it does not make\n      // the whole page interactive.\n      setHovering(\n        Boolean(interactive) &&\n          interactive !== document.body &&\n          interactive !== document.documentElement\n      );\n      setLabelKey(element?.closest(\"[data-cursor]\")?.getAttribute(\"data-cursor\") ?? null);\n    };\n    const onOut = (event: PointerEvent) => {\n      // No element to go to: the pointer left the window.\n      if (!event.relatedTarget) setVisible(false);\n    };\n    const onDown = () => setPressed(true);\n    const onUp = () => setPressed(false);\n    const onBlur = () => {\n      setVisible(false);\n      setPressed(false);\n    };\n\n    window.addEventListener(\"pointermove\", onMove, { passive: true });\n    window.addEventListener(\"pointerdown\", onDown, { passive: true });\n    window.addEventListener(\"pointerup\", onUp, { passive: true });\n    window.addEventListener(\"pointercancel\", onUp, { passive: true });\n    document.addEventListener(\"pointerover\", onOver, { passive: true });\n    document.addEventListener(\"pointerout\", onOut, { passive: true });\n    window.addEventListener(\"blur\", onBlur);\n    return () => {\n      cancelAnimationFrame(frame);\n      window.removeEventListener(\"pointermove\", onMove);\n      window.removeEventListener(\"pointerdown\", onDown);\n      window.removeEventListener(\"pointerup\", onUp);\n      window.removeEventListener(\"pointercancel\", onUp);\n      document.removeEventListener(\"pointerover\", onOver);\n      document.removeEventListener(\"pointerout\", onOut);\n      window.removeEventListener(\"blur\", onBlur);\n    };\n  }, []);\n\n  const state: CustomCursorState = !visible\n    ? \"hidden\"\n    : pressed\n      ? \"pressed\"\n      : hovering\n        ? \"hover\"\n        : \"default\";\n  const label = labelKey !== null ? labels?.[labelKey] : undefined;\n  // The ring is drawn at its hover size and scaled down, so it grows with a\n  // transform; the border is scaled back up to stay 1px.\n  const scale = (hovering ? 1 : size / Math.max(hoverSize, 1)) * (pressed ? 0.8 : 1);\n\n  return createPortal(\n    <div\n      ref={root}\n      aria-hidden=\"true\"\n      data-slot=\"custom-cursor\"\n      data-state={state}\n      data-label={label !== undefined ? labelKey : undefined}\n      className={cn(\n        \"pointer-events-none fixed top-0 left-0 z-[9999] transition-opacity data-[state=hidden]:opacity-0 motion-reduce:transition-none\",\n        blend ? \"text-white mix-blend-difference\" : \"text-foreground\",\n        className\n      )}\n      {...props}\n    >\n      {hideNativeCursor ? <style>{HIDE_NATIVE_CURSOR}</style> : null}\n      <div ref={ring} data-slot=\"custom-cursor-ring\" className=\"absolute top-0 left-0\">\n        <div\n          data-slot=\"custom-cursor-ring-shape\"\n          className=\"grid place-items-center rounded-full border border-current transition-[transform,border-width] duration-300 ease-out motion-reduce:transition-none\"\n          style={{\n            width: hoverSize,\n            height: hoverSize,\n            transform: `scale(${scale})`,\n            borderWidth: `${1 / scale}px`,\n          }}\n        >\n          {label !== undefined ? (\n            <span data-slot=\"custom-cursor-label\" className=\"text-xs font-medium\">\n              {label}\n            </span>\n          ) : null}\n        </div>\n      </div>\n      {dotSize > 0 ? (\n        <div\n          ref={dot}\n          data-slot=\"custom-cursor-dot\"\n          className=\"absolute top-0 left-0 rounded-full bg-current transition-opacity motion-reduce:transition-none\"\n          style={{ width: dotSize, height: dotSize, opacity: hovering ? 0 : 1 }}\n        />\n      ) : null}\n      {children}\n    </div>,\n    document.body\n  );\n}\n\nexport { CustomCursor, type CustomCursorProps, type CustomCursorState };\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "motion"
  ],
  "type": "registry:ui"
}