simulated-cursor
remotionui/simulated-cursorFreeComponent
Frame-timed cursor with press, click ripples, hit targets and labels
Live preview
live · rendered by the registry
Rendered by remotionui on their own page — this is the component running, not a screenshot of it.Install it
npx shadcn@latest add https://remotionui.com/r/simulated-cursor.jsonSource
1import { interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion";2import { springSmooth, springSnappy } from "@/remotion/lib/springs";34export type SimulatedCursorPoint = {5 /** Percentage of the frame width — 0 is the left edge, 100 the right. */6 x: number;7 /** Percentage of the frame height. */8 y: number;9 /** Frame the cursor arrives at this point. */10 frame: number;11 /** Chip shown next to the cursor while it rests here. */12 label?: string;13 /** Ring drawn under the cursor, sized in pixels — marks the hit target. */14 target?: number;15};1617export type SimulatedCursorProps = {18 points?: SimulatedCursorPoint[];19 color?: string;20 accent?: string;21 size?: number;22 clickFrames?: number[];23};2425const DEFAULT_POINTS: SimulatedCursorPoint[] = [26 { x: 18, y: 72, frame: 0 },27 { x: 42, y: 58, frame: 24 },28 { x: 68, y: 44, frame: 48, target: 72 },29];3031const CLICK_DURATION = 16;32/** How long a press holds the pointer down before it springs back. */33const PRESS_FRAMES = 5;3435export const SimulatedCursor: React.FC<SimulatedCursorProps> = ({36 points = DEFAULT_POINTS,37 color = "#f4f4f5",38 accent = "#e8b86d",39 size = 26,40 clickFrames = [48],41}) => {42 const frame = useCurrentFrame();43 const { fps, width, height } = useVideoConfig();4445 const first = points[0] ?? { x: 50, y: 50, frame: 0 };46 const last = points[points.length - 1] ?? first;4748 // Each hop is its own spring, so the cursor settles into a target the way a49 // hand does — decelerating hard — instead of gliding at a constant rate.50 let position = { x: first.x, y: first.y };51 let resting: SimulatedCursorPoint = first;5253 for (let index = 0; index < points.length - 1; index += 1) {54 const from = points[index];55 const to = points[index + 1];5657 if (frame >= to.frame) {58 continue;59 }60 if (frame < from.frame) {61 break;62 }6364 const travel = spring({65 frame: frame - from.frame,66 fps,67 config: springSmooth,68 durationInFrames: Math.max(1, to.frame - from.frame),69 });70 position = {71 x: interpolate(travel, [0, 1], [from.x, to.x]),72 y: interpolate(travel, [0, 1], [from.y, to.y]),73 };74 resting = travel > 0.75 ? to : from;75 break;76 }7778 if (frame >= last.frame) {79 position = { x: last.x, y: last.y };80 resting = last;81 }8283 const activeClick = clickFrames84 .filter((clickFrame) => frame >= clickFrame)85 .pop();86 const clickPulse =87 activeClick === undefined88 ? 089 : spring({90// … truncated
More from remotionui
All 127 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| audio-pulseaudio-pulse | remotionui | Components | Free | no deps | |
| audiogram-barsaudiogram-bars | remotionui | Components | Free | no deps | |
| blur-focus-inblur-focus-in | remotionui | Components | Free | no deps | |
| blur-inblur-in | remotionui | Components | Free | no deps | |
| blur-revealblur-reveal | remotionui | Components | Free | no deps | |
| caption-highlightcaption-highlight | remotionui | Components | Free | no deps | |
| chromatic-aberration-wipechromatic-aberration-wipe | remotionui | Components | Free | no deps | |
| confetti-burstconfetti-burst | remotionui | Components | Free | no deps |
