{
  "name": "simulated-cursor",
  "type": "registry:ui",
  "description": "Frame-timed cursor with press, click ripples, hit targets and labels",
  "dependencies": [
    "remotion"
  ],
  "registryDependencies": [
    "springs"
  ],
  "files": [
    {
      "path": "registry/bases/default/primitives/simulated-cursor.tsx",
      "type": "registry:ui",
      "content": "import { interpolate, spring, useCurrentFrame, useVideoConfig } from \"remotion\";\nimport { springSmooth, springSnappy } from \"@/remotion/lib/springs\";\n\nexport type SimulatedCursorPoint = {\n  /** Percentage of the frame width — 0 is the left edge, 100 the right. */\n  x: number;\n  /** Percentage of the frame height. */\n  y: number;\n  /** Frame the cursor arrives at this point. */\n  frame: number;\n  /** Chip shown next to the cursor while it rests here. */\n  label?: string;\n  /** Ring drawn under the cursor, sized in pixels — marks the hit target. */\n  target?: number;\n};\n\nexport type SimulatedCursorProps = {\n  points?: SimulatedCursorPoint[];\n  color?: string;\n  accent?: string;\n  size?: number;\n  clickFrames?: number[];\n};\n\nconst DEFAULT_POINTS: SimulatedCursorPoint[] = [\n  { x: 18, y: 72, frame: 0 },\n  { x: 42, y: 58, frame: 24 },\n  { x: 68, y: 44, frame: 48, target: 72 },\n];\n\nconst CLICK_DURATION = 16;\n/** How long a press holds the pointer down before it springs back. */\nconst PRESS_FRAMES = 5;\n\nexport const SimulatedCursor: React.FC<SimulatedCursorProps> = ({\n  points = DEFAULT_POINTS,\n  color = \"#f4f4f5\",\n  accent = \"#e8b86d\",\n  size = 26,\n  clickFrames = [48],\n}) => {\n  const frame = useCurrentFrame();\n  const { fps, width, height } = useVideoConfig();\n\n  const first = points[0] ?? { x: 50, y: 50, frame: 0 };\n  const last = points[points.length - 1] ?? first;\n\n  // Each hop is its own spring, so the cursor settles into a target the way a\n  // hand does — decelerating hard — instead of gliding at a constant rate.\n  let position = { x: first.x, y: first.y };\n  let resting: SimulatedCursorPoint = first;\n\n  for (let index = 0; index < points.length - 1; index += 1) {\n    const from = points[index];\n    const to = points[index + 1];\n\n    if (frame >= to.frame) {\n      continue;\n    }\n    if (frame < from.frame) {\n      break;\n    }\n\n    const travel = spring({\n      frame: frame - from.frame,\n      fps,\n      config: springSmooth,\n      durationInFrames: Math.max(1, to.frame - from.frame),\n    });\n    position = {\n      x: interpolate(travel, [0, 1], [from.x, to.x]),\n      y: interpolate(travel, [0, 1], [from.y, to.y]),\n    };\n    resting = travel > 0.75 ? to : from;\n    break;\n  }\n\n  if (frame >= last.frame) {\n    position = { x: last.x, y: last.y };\n    resting = last;\n  }\n\n  const activeClick = clickFrames\n    .filter((clickFrame) => frame >= clickFrame)\n    .pop();\n  const clickPulse =\n    activeClick === undefined\n      ? 0\n      : spring({\n          frame: frame - activeClick,\n          fps,\n          config: springSnappy,\n          durationInFrames: CLICK_DURATION,\n        });\n  const clicking = clickPulse > 0 && clickPulse < 1;\n  const pressed =\n    activeClick !== undefined && frame - activeClick < PRESS_FRAMES;\n\n  const left = (position.x / 100) * width;\n  const top = (position.y / 100) * height;\n  const showTarget = resting.target !== undefined;\n  // The label belongs to the point the cursor is arriving at, so it fades in\n  // just before touchdown rather than after the cursor has already stopped.\n  const labelOpacity = resting.label\n    ? interpolate(frame, [resting.frame - 8, resting.frame - 1], [0, 1], {\n        extrapolateLeft: \"clamp\",\n        extrapolateRight: \"clamp\",\n      })\n    : 0;\n\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        left,\n        top,\n        pointerEvents: \"none\",\n        zIndex: 20,\n      }}\n    >\n      {showTarget ? (\n        <div\n          style={{\n            position: \"absolute\",\n            left: -(resting.target ?? 0) / 2 + size * 0.2,\n            top: -(resting.target ?? 0) / 2 + size * 0.2,\n            width: resting.target,\n            height: resting.target,\n            borderRadius: 999,\n            border: `2px solid ${accent}`,\n            opacity: clicking ? 0.75 : 0.32,\n          }}\n        />\n      ) : null}\n\n      {clicking ? (\n        <div\n          style={{\n            position: \"absolute\",\n            left: size * 0.2 - clickPulse * 26,\n            top: size * 0.2 - clickPulse * 26,\n            width: 12 + clickPulse * 52,\n            height: 12 + clickPulse * 52,\n            borderRadius: 999,\n            border: `2px solid ${accent}`,\n            opacity: Math.max(0, 0.8 - clickPulse * 0.8),\n          }}\n        />\n      ) : null}\n\n      <svg\n        width={size}\n        height={size}\n        viewBox=\"0 0 24 24\"\n        fill=\"none\"\n        style={{\n          display: \"block\",\n          scale: pressed ? 0.88 : 1,\n          filter: \"drop-shadow(0 4px 12px rgba(0,0,0,0.45))\",\n        }}\n      >\n        <path\n          d=\"M5 3L19 12L12 13L9 20L5 3Z\"\n          fill={color}\n          stroke=\"#080810\"\n          strokeWidth={1.2}\n          strokeLinejoin=\"round\"\n        />\n      </svg>\n\n      {resting.label ? (\n        <div\n          style={{\n            position: \"absolute\",\n            left: size * 0.9,\n            top: size * 0.9,\n            padding: \"6px 12px\",\n            borderRadius: 6,\n            background: \"rgba(8,8,16,0.92)\",\n            border: \"1px solid rgba(255,255,255,0.12)\",\n            color: \"#ececec\",\n            fontSize: 18,\n            fontWeight: 500,\n            whiteSpace: \"nowrap\",\n            opacity: labelOpacity,\n          }}\n        >\n          {resting.label}\n        </div>\n      ) : null}\n    </div>\n  );\n};\n"
    }
  ],
  "atlas": {
    "lane": "vectors",
    "drive": "time",
    "tier": "core"
  }
}