{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "cursor-flow", "title": "Cursor Flow", "description": "A realistic mouse that arcs along cubic Bezier paths, pauses, and clicks targets that dip in response.", "dependencies": [ "remotion" ], "files": [ { "path": "registry/remocn/cursor-flow/index.tsx", "content": "\"use client\";\n\nimport { interpolate, spring, useCurrentFrame, useVideoConfig } from \"remotion\";\n\nexport interface CursorWaypoint {\n x: number;\n y: number;\n /** If set, the cursor will pause here for this many frames before moving on. */\n hold?: number;\n /** If true, a click animation will fire when the cursor reaches this point. */\n click?: boolean;\n /** Optional label rendered next to the click target. */\n label?: string;\n}\n\nexport interface CursorFlowProps {\n waypoints?: CursorWaypoint[];\n cursorColor?: string;\n cursorSize?: number;\n segmentDuration?: number;\n background?: string;\n showTargets?: boolean;\n speed?: number;\n className?: string;\n}\n\nconst FONT_FAMILY =\n \"var(--font-geist-sans), -apple-system, BlinkMacSystemFont, sans-serif\";\n\nconst DEFAULT_WAYPOINTS: CursorWaypoint[] = [\n { x: 200, y: 180 },\n { x: 540, y: 240, click: true, label: \"Generate\" },\n { x: 880, y: 360, hold: 8 },\n { x: 1040, y: 520, click: true, label: \"Publish\" },\n];\n\n/**\n * Cubic Bezier evaluator. Two control points are derived per segment so the\n * trajectory always has a \"belly\" — straight lines between two points are\n * specifically the look we want to avoid.\n */\nfunction cubicBezier(\n t: number,\n p0: { x: number; y: number },\n p1: { x: number; y: number },\n p2: { x: number; y: number },\n p3: { x: number; y: number },\n) {\n const u = 1 - t;\n const tt = t * t;\n const uu = u * u;\n const uuu = uu * u;\n const ttt = tt * t;\n return {\n x: uuu * p0.x + 3 * uu * t * p1.x + 3 * u * tt * p2.x + ttt * p3.x,\n y: uuu * p0.y + 3 * uu * t * p1.y + 3 * u * tt * p2.y + ttt * p3.y,\n };\n}\n\n/** Apple-flavoured ease for in-segment motion: slow start, slow end. */\nfunction easeInOut(t: number) {\n return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;\n}\n\n/** Per-segment control points: midpoint perpendicular offset gives a natural arc. */\nfunction controlsFor(\n a: { x: number; y: number },\n b: { x: number; y: number },\n index: number,\n) {\n const dx = b.x - a.x;\n const dy = b.y - a.y;\n const len = Math.hypot(dx, dy) || 1;\n // Perpendicular unit vector. Alternate sign per segment so the path snakes\n // instead of always bulging the same way.\n const sign = index % 2 === 0 ? 1 : -1;\n const px = (-dy / len) * (len * 0.18) * sign;\n const py = (dx / len) * (len * 0.18) * sign;\n // Place control points at 1/3 and 2/3 along the chord, offset perpendicular.\n return [\n { x: a.x + dx / 3 + px, y: a.y + dy / 3 + py },\n { x: a.x + (2 * dx) / 3 + px, y: a.y + (2 * dy) / 3 + py },\n ];\n}\n\nexport function CursorFlow({\n waypoints = DEFAULT_WAYPOINTS,\n cursorColor = \"#fafafa\",\n cursorSize = 28,\n segmentDuration = 36,\n background = \"#0a0a0a\",\n showTargets = true,\n speed = 1,\n className,\n}: CursorFlowProps) {\n const frame = useCurrentFrame() * speed;\n const { fps } = useVideoConfig();\n\n // Build the timeline: each segment is segmentDuration frames, with optional\n // hold time at each waypoint.\n type SegmentTiming = {\n startFrame: number;\n endFrame: number;\n holdEnd: number;\n };\n const timings: SegmentTiming[] = [];\n let cursorFrame = 0;\n for (let i = 0; i < waypoints.length - 1; i++) {\n const start = cursorFrame;\n const end = start + segmentDuration;\n const holdEnd = end + (waypoints[i + 1].hold ?? 0);\n timings.push({ startFrame: start, endFrame: end, holdEnd });\n cursorFrame = holdEnd;\n }\n\n // Find the active segment.\n let pos = { x: waypoints[0].x, y: waypoints[0].y };\n let activeWaypointIndex = 0;\n for (let i = 0; i < timings.length; i++) {\n const { startFrame, endFrame, holdEnd } = timings[i];\n if (frame >= startFrame && frame < endFrame) {\n const t = easeInOut((frame - startFrame) / segmentDuration);\n const a = waypoints[i];\n const b = waypoints[i + 1];\n const [c1, c2] = controlsFor(a, b, i);\n pos = cubicBezier(t, a, c1, c2, b);\n activeWaypointIndex = i;\n break;\n } else if (frame >= endFrame && frame < holdEnd) {\n pos = { x: waypoints[i + 1].x, y: waypoints[i + 1].y };\n activeWaypointIndex = i + 1;\n break;\n } else if (frame >= holdEnd) {\n pos = { x: waypoints[i + 1].x, y: waypoints[i + 1].y };\n activeWaypointIndex = i + 1;\n }\n }\n\n return (\n