{
  "name": "cursor-path",
  "type": "registry:ui",
  "description": "Cursor travelling a path with drawn trail and click ripples",
  "dependencies": [
    "remotion",
    "@remotion/paths"
  ],
  "registryDependencies": [
    "path-utils",
    "motion-tokens",
    "springs"
  ],
  "files": [
    {
      "path": "registry/bases/default/primitives/cursor-path.tsx",
      "type": "registry:ui",
      "content": "import { interpolate, spring, useCurrentFrame, useVideoConfig } from \"remotion\";\nimport { EASING } from \"@/remotion/lib/motion-tokens\";\nimport { springSnappy } from \"@/remotion/lib/springs\";\nimport {\n  clampProgress,\n  getPathDrawStyles,\n  samplePath,\n  waypointProgress,\n  waypointsToPath,\n  type PathPoint,\n} from \"@/remotion/lib/path-utils\";\n\nexport type CursorPoint = PathPoint;\n\nexport type CursorPathProps = {\n  /** Waypoints in the parent's coordinate space. Ignored when `d` is set. */\n  points?: CursorPoint[];\n  /** Drive the cursor along an authored SVG path instead of waypoints. */\n  d?: string;\n  durationInFrames?: number;\n  delayInFrames?: number;\n  color?: string;\n  size?: number;\n  /** 0 hops in straight lines; higher values round the corners. */\n  smoothing?: number;\n  /** `draw` reveals the route behind the cursor, `guide` shows it up front. */\n  trail?: \"draw\" | \"guide\" | \"none\";\n  /** Waypoint indices that get a click ripple as the cursor arrives. */\n  clickAt?: number[];\n};\n\nconst CLICK_DURATION = 18;\n\nexport const CursorPath: React.FC<CursorPathProps> = ({\n  points = [],\n  d,\n  durationInFrames = 90,\n  delayInFrames = 0,\n  color = \"#e8b86d\",\n  size = 34,\n  smoothing = 0.6,\n  trail = \"draw\",\n  clickAt = [],\n}) => {\n  const frame = useCurrentFrame();\n  const { fps } = useVideoConfig();\n  const path = d ?? waypointsToPath(points, smoothing);\n\n  if (!path) {\n    return null;\n  }\n\n  // Travel is measured in arc length, not waypoint index, so the cursor holds\n  // one speed across a long hop and a short one.\n  const progress = clampProgress(\n    interpolate(\n      frame,\n      [delayInFrames, delayInFrames + durationInFrames],\n      [0, 1],\n      {\n        extrapolateLeft: \"clamp\",\n        extrapolateRight: \"clamp\",\n        easing: EASING.editorial,\n      },\n    ),\n  );\n  const position = samplePath(path, progress);\n  const trailStyles = getPathDrawStyles(progress, path);\n\n  // A click fires on arrival, so its frame comes from where the waypoint sits\n  // along the path rather than from a hand-timed offset.\n  const stops = d ? [] : waypointProgress(path, points);\n  const clicks = clickAt\n    .filter((index) => stops[index] !== undefined)\n    .map((index) => ({\n      point: points[index],\n      startFrame: delayInFrames + stops[index] * durationInFrames,\n    }));\n\n  return (\n    <svg style={{ position: \"absolute\", inset: 0, overflow: \"visible\" }}>\n      {trail === \"guide\" ? (\n        <path\n          d={path}\n          fill=\"none\"\n          stroke={color}\n          strokeWidth={2.5}\n          strokeOpacity={0.28}\n          strokeDasharray=\"6 10\"\n          strokeLinecap=\"round\"\n        />\n      ) : null}\n\n      {trail === \"draw\" ? (\n        <>\n          <path\n            d={path}\n            fill=\"none\"\n            stroke={color}\n            strokeWidth={3}\n            strokeOpacity={0.18}\n            strokeLinecap=\"round\"\n          />\n          <path\n            d={path}\n            fill=\"none\"\n            stroke={color}\n            strokeWidth={5}\n            strokeOpacity={0.8}\n            strokeLinecap=\"round\"\n            strokeDasharray={trailStyles.strokeDasharray}\n            strokeDashoffset={trailStyles.strokeDashoffset}\n          />\n        </>\n      ) : null}\n\n      {clicks.map(({ point, startFrame }, index) => {\n        const pulse = spring({\n          frame: frame - startFrame,\n          fps,\n          config: springSnappy,\n          durationInFrames: CLICK_DURATION,\n        });\n        if (pulse <= 0 || pulse >= 1) {\n          return null;\n        }\n\n        return (\n          <circle\n            key={`click-${index}`}\n            cx={point.x}\n            cy={point.y}\n            r={6 + pulse * size * 0.9}\n            fill=\"none\"\n            stroke={color}\n            strokeWidth={2}\n            opacity={0.7 - pulse * 0.7}\n          />\n        );\n      })}\n\n      <g transform={`translate(${position.x} ${position.y})`}>\n        <path\n          d={[\n            \"M0 0\",\n            `L0 ${size}`,\n            `L${size * 0.26} ${size * 0.74}`,\n            `L${size * 0.47} ${size}`,\n            `L${size * 0.65} ${size * 0.88}`,\n            `L${size * 0.44} ${size * 0.56}`,\n            `L${size * 0.74} ${size * 0.56}`,\n            \"Z\",\n          ].join(\" \")}\n          fill=\"#f8fafc\"\n          stroke=\"#080810\"\n          strokeWidth={size * 0.06}\n          strokeLinejoin=\"round\"\n        />\n      </g>\n    </svg>\n  );\n};\n"
    }
  ],
  "atlas": {
    "lane": "vectors",
    "drive": "time",
    "tier": "core"
  }
}