{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "cursor",
  "title": "UI Cursor",
  "description": "An animated cursor that moves between waypoints, clicks (ripple), and drags. The Cursor renderer is a pure function of a numeric CursorStyle channel; useCursorPath reads the frame and eases the path, ripple, and press phases.",
  "dependencies": ["remotion"],
  "registryDependencies": ["@remocn/remocn-ui"],
  "files": [
    {
      "path": "registry/remocn-ui/cursor/index.tsx",
      "content": "\"use client\";\n\nimport { type RemocnTheme, useRemocnTheme } from \"@/lib/remocn-ui\";\n\nexport type CursorVariant = \"arrow\" | \"pointer\";\n\nexport interface CursorStyle {\n  x: number;\n  y: number;\n  scale: number;\n  rippleOpacity: number;\n  rippleScale: number;\n  pressScale?: number;\n}\n\nexport interface CursorProps {\n  style?: CursorStyle;\n  variant?: CursorVariant;\n  size?: number;\n  theme?: Partial<RemocnTheme>;\n  rippleColor?: string;\n  className?: string;\n}\n\nconst REST: CursorStyle = {\n  x: 0,\n  y: 0,\n  scale: 1,\n  rippleOpacity: 0,\n  rippleScale: 0,\n};\n\nconst ARROW_PATH =\n  \"M1 1 L1 18.5 L5.6 14.4 L8.7 21.6 L11.6 20.4 L8.5 13.2 L14.5 13.2 Z\";\nconst POINTER_PATH =\n  \"M8 2.2 C8 1.3 8.7 0.7 9.5 0.7 C10.3 0.7 11 1.3 11 2.2 L11 9 \" +\n  \"C11 9 11.3 8.4 12.2 8.4 C13 8.4 13.4 9 13.4 9.6 \" +\n  \"C13.4 9.6 13.9 9.1 14.7 9.1 C15.5 9.1 15.9 9.7 15.9 10.3 \" +\n  \"C15.9 10.3 16.4 9.9 17.1 9.9 C17.9 9.9 18.3 10.5 18.3 11.2 \" +\n  \"L18.3 16.8 C18.3 19.8 16.3 22.3 12.8 22.3 L11.4 22.3 \" +\n  \"C9 22.3 7.9 21.2 6.6 19.2 L4 15.2 C3.5 14.4 3.7 13.4 4.5 12.9 \" +\n  \"C5.1 12.5 5.9 12.6 6.4 13.2 L8 15 Z\";\n\nexport function Cursor({\n  style,\n  variant = \"arrow\",\n  size = 28,\n  theme: themeOverride,\n  rippleColor,\n  className,\n}: CursorProps) {\n  const theme = useRemocnTheme(themeOverride, \"light\");\n  const v = style ?? REST;\n\n  const press = v.pressScale ?? 1;\n  const pressedScale = v.scale * (0.9 + 0.1 * press);\n\n  const ring = rippleColor ?? theme.primary;\n  const fill = theme.foreground;\n  const stroke = theme.background;\n  const rippleSize = size * 1.8;\n\n  return (\n    <div\n      className={className}\n      style={{\n        position: \"absolute\",\n        left: 0,\n        top: 0,\n        transform: `translate(${v.x}px, ${v.y}px)`,\n        pointerEvents: \"none\",\n      }}\n    >\n      {}\n      <div\n        style={{\n          position: \"absolute\",\n          left: -rippleSize / 2,\n          top: -rippleSize / 2,\n          width: rippleSize,\n          height: rippleSize,\n          borderRadius: \"50%\",\n          border: `2px solid ${ring}`,\n          opacity: v.rippleOpacity,\n          transform: `scale(${v.rippleScale})`,\n          transformOrigin: \"center\",\n        }}\n      />\n      {}\n      <svg\n        width={size}\n        height={size}\n        viewBox=\"0 0 24 24\"\n        fill=\"none\"\n        style={{\n          position: \"absolute\",\n          left: 0,\n          top: 0,\n          transform: `scale(${pressedScale})`,\n          transformOrigin: \"0 0\",\n          filter: \"drop-shadow(0 1px 2px rgba(0,0,0,0.3))\",\n          overflow: \"visible\",\n        }}\n      >\n        <path\n          d={variant === \"pointer\" ? POINTER_PATH : ARROW_PATH}\n          fill={fill}\n          stroke={stroke}\n          strokeWidth={1.4}\n          strokeLinejoin=\"round\"\n        />\n      </svg>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/cursor.tsx"
    },
    {
      "path": "registry/remocn-ui/cursor/use-cursor-path.ts",
      "content": "\"use client\";\n\nimport { useCurrentFrame } from \"remotion\";\nimport type { CursorStyle } from \"@/components/remocn/cursor\";\nimport { clamp01, type EasingName, easings } from \"@/lib/remocn-ui\";\n\nexport interface CursorWaypoint {\n  at: number;\n  x: number;\n  y: number;\n  duration?: number;\n  click?: boolean;\n  press?: boolean;\n  easing?: EasingName;\n}\n\nexport const DEFAULT_DURATION = 24;\n\nexport const CLICK_FRAMES = 16;\n\nexport const PRESS_FRAMES = 8;\n\nexport interface CursorPathOptions {\n  speed?: number;\n  defaultDuration?: number;\n}\n\nfunction lerp(a: number, b: number, t: number): number {\n  return a + (b - a) * t;\n}\n\nexport function ripplePhase(framesSinceClick: number): {\n  rippleOpacity: number;\n  rippleScale: number;\n} {\n  if (framesSinceClick < 0 || framesSinceClick >= CLICK_FRAMES) {\n    return { rippleOpacity: 0, rippleScale: 0 };\n  }\n  const p = clamp01(framesSinceClick / CLICK_FRAMES);\n  return {\n    rippleOpacity: 0.5 * (1 - p),\n    rippleScale: 2.5 * easings.out(p),\n  };\n}\n\nexport function clickPress(framesSinceClick: number): number {\n  if (framesSinceClick < 0 || framesSinceClick >= PRESS_FRAMES) return 1;\n  const half = PRESS_FRAMES / 2;\n  const p =\n    framesSinceClick < half\n      ? framesSinceClick / half\n      : 1 - (framesSinceClick - half) / half;\n  return 1 - clamp01(p);\n}\n\nexport function useCursorPath(\n  waypoints: CursorWaypoint[],\n  opts: CursorPathOptions = {},\n): CursorStyle {\n  const { speed = 1 } = opts;\n  const raw = useCurrentFrame() * speed;\n  return cursorPathAt(waypoints, raw, opts);\n}\n\nexport function cursorPathAt(\n  waypoints: CursorWaypoint[],\n  raw: number,\n  opts: CursorPathOptions = {},\n): CursorStyle {\n  const { defaultDuration = DEFAULT_DURATION } = opts;\n\n  if (waypoints.length === 0) {\n    return { x: 0, y: 0, scale: 1, rippleOpacity: 0, rippleScale: 0 };\n  }\n\n  const first = waypoints[0];\n\n  if (raw <= first.at) {\n    return {\n      x: first.x,\n      y: first.y,\n      scale: 1,\n      rippleOpacity: 0,\n      rippleScale: 0,\n      pressScale: 1,\n    };\n  }\n\n  let toIndex = waypoints.length - 1;\n  for (let i = 1; i < waypoints.length; i++) {\n    if (waypoints[i].at > raw) {\n      toIndex = i;\n      break;\n    }\n  }\n  const pastLast = raw >= waypoints[waypoints.length - 1].at;\n  const to = pastLast ? waypoints[waypoints.length - 1] : waypoints[toIndex];\n  const from = pastLast\n    ? waypoints[waypoints.length - 1]\n    : waypoints[toIndex - 1];\n\n  const dur = to.duration ?? defaultDuration;\n  const ease = easings[to.easing ?? \"inOut\"];\n  const start = to.at - dur;\n  const t = pastLast || dur <= 0 ? 1 : ease(clamp01((raw - start) / dur));\n  const x = lerp(from.x, to.x, t);\n  const y = lerp(from.y, to.y, t);\n\n  let lastClickAt = -Infinity;\n  for (const wp of waypoints) {\n    if (wp.click && wp.at <= raw && wp.at > lastClickAt) lastClickAt = wp.at;\n  }\n  const sinceClick = lastClickAt === -Infinity ? -1 : raw - lastClickAt;\n  const ripple = ripplePhase(sinceClick);\n  const clickDip = clickPress(sinceClick);\n\n  const holdWp = pastLast ? waypoints[waypoints.length - 1] : from;\n  const heldPress = holdWp.press ? 0 : 1;\n\n  const pressScale = Math.min(clickDip, heldPress);\n\n  return {\n    x,\n    y,\n    scale: 1,\n    rippleOpacity: ripple.rippleOpacity,\n    rippleScale: ripple.rippleScale,\n    pressScale,\n  };\n}\n",
      "type": "registry:component",
      "target": "components/remocn/use-cursor-path.ts"
    }
  ],
  "type": "registry:component"
}
