{ "$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 background?: string;\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 background = \"#0a0a0a\",\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