{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "simulated-cursor",
  "title": "Simulated Cursor",
  "description": "Animated mouse cursor moving between waypoints with click ripple feedback.",
  "dependencies": ["remotion"],
  "files": [
    {
      "path": "registry/remocn/simulated-cursor/index.tsx",
      "content": "\"use client\";\n\nimport { interpolate, spring, useCurrentFrame, useVideoConfig } from \"remotion\";\n\nexport interface CursorPoint {\n  x: number;\n  y: number;\n  hold?: number;\n  click?: boolean;\n}\n\nexport interface SimulatedCursorProps {\n  points?: CursorPoint[];\n  color?: string;\n  size?: number;\n  speed?: number;\n  className?: string;\n}\n\nconst DEFAULT_POINTS: CursorPoint[] = [\n  { x: 200, y: 150, hold: 20 },\n  { x: 800, y: 360, hold: 25, click: true },\n  { x: 1050, y: 560, hold: 20 },\n];\n\nexport function SimulatedCursor({\n  points = DEFAULT_POINTS,\n  color = \"#ffffff\",\n  size = 32,\n  speed = 1,\n  className,\n}: SimulatedCursorProps) {\n  const frame = useCurrentFrame() * speed;\n  const { fps, durationInFrames } = useVideoConfig();\n\n  const travelPerLeg = 24;\n  const segments: Array<{\n    start: number;\n    end: number;\n    holdEnd: number;\n    from: CursorPoint;\n    to: CursorPoint;\n  }> = [];\n\n  let cursor = 0;\n  for (let i = 0; i < points.length - 1; i++) {\n    const from = points[i];\n    const to = points[i + 1];\n    const start = cursor;\n    const end = start + travelPerLeg;\n    const holdEnd = end + (to.hold ?? 15);\n    segments.push({ start, end, holdEnd, from, to });\n    cursor = holdEnd;\n  }\n\n  // Find current segment\n  let x = points[0].x;\n  let y = points[0].y;\n  let activeClickFrame: number | null = null;\n  let clickTarget: CursorPoint | null = null;\n\n  if (segments.length === 0) {\n    x = points[0].x;\n    y = points[0].y;\n  } else if (frame < segments[0].start + (points[0].hold ?? 0)) {\n    x = points[0].x;\n    y = points[0].y;\n  } else {\n    for (const seg of segments) {\n      if (frame >= seg.start && frame < seg.holdEnd) {\n        if (frame < seg.end) {\n          // travelling\n          x = interpolate(frame, [seg.start, seg.end], [seg.from.x, seg.to.x], {\n            extrapolateLeft: \"clamp\",\n            extrapolateRight: \"clamp\",\n          });\n          y = interpolate(frame, [seg.start, seg.end], [seg.from.y, seg.to.y], {\n            extrapolateLeft: \"clamp\",\n            extrapolateRight: \"clamp\",\n          });\n        } else {\n          x = seg.to.x;\n          y = seg.to.y;\n          if (seg.to.click) {\n            activeClickFrame = frame - seg.end;\n            clickTarget = seg.to;\n          }\n        }\n        break;\n      }\n      if (frame >= seg.holdEnd) {\n        x = seg.to.x;\n        y = seg.to.y;\n      }\n    }\n  }\n\n  // Click feedback\n  let clickScale = 1;\n  let rippleRadius = 0;\n  let rippleOpacity = 0;\n  if (activeClickFrame !== null && clickTarget) {\n    const dip = spring({\n      fps,\n      frame: activeClickFrame,\n      config: { damping: 10, stiffness: 200, mass: 0.6 },\n      durationInFrames: 14,\n    });\n    clickScale = 1 - dip * 0.18;\n    rippleRadius = interpolate(activeClickFrame, [0, 24], [4, 60], {\n      extrapolateLeft: \"clamp\",\n      extrapolateRight: \"clamp\",\n    });\n    rippleOpacity = interpolate(activeClickFrame, [0, 24], [0.6, 0], {\n      extrapolateLeft: \"clamp\",\n      extrapolateRight: \"clamp\",\n    });\n  }\n\n  // Suppress unused warning when durationInFrames not used directly\n  void durationInFrames;\n\n  return (\n    <div\n      className={className}\n      style={{\n        position: \"absolute\",\n        inset: 0,\n        overflow: \"hidden\",\n      }}\n    >\n      {/* Click ripple */}\n      {rippleOpacity > 0 && (\n        <svg\n          style={{\n            position: \"absolute\",\n            left: x - 80,\n            top: y - 80,\n            width: 160,\n            height: 160,\n            pointerEvents: \"none\",\n          }}\n        >\n          <circle\n            cx={80}\n            cy={80}\n            r={rippleRadius}\n            fill=\"none\"\n            stroke={color}\n            strokeWidth={2}\n            opacity={rippleOpacity}\n          />\n        </svg>\n      )}\n\n      {/* Cursor */}\n      <div\n        style={{\n          position: \"absolute\",\n          left: x,\n          top: y,\n          width: size,\n          height: size,\n          scale: `${clickScale}`,\n          transformOrigin: \"top left\",\n          zIndex: 2147483647,\n          pointerEvents: \"none\",\n          willChange: \"transform, left, top\",\n        }}\n      >\n        <svg\n          width={size}\n          height={size}\n          viewBox=\"0 0 24 24\"\n          fill=\"none\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n        >\n          <path\n            d=\"M5 3L5 19L9.5 14.5L12.5 21L15 20L12 13.5L18.5 13.5L5 3Z\"\n            fill={color}\n            stroke=\"#000000\"\n            strokeWidth={1.2}\n            strokeLinejoin=\"round\"\n          />\n        </svg>\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/simulated-cursor.tsx"
    }
  ],
  "type": "registry:component"
}
