Cursor Gravity

A rounded triangular cursor tugs a button out of the screen edge with pulsed elastic resistance

Installation
$ pnpm dlx shadcn@latest add @remocn/cursor-gravity

Usage

An empty scene. A soft triangular cursor arrives from below, approaches the right edge and catches a hidden button. Two quick tugs pull its leading edge into view, pause under load, and recoil. The button's back remains outside the frame, stretching the surface into a narrow neck. A final pull frees the whole button, which settles at center as the cursor exits below.

import { CursorGravity } from "@/components/remocn/cursor-gravity";
 
export const Scene = () => <CursorGravity label="Create something" />;

Tension controls the recoil and surface deformation. The cursor’s rear bows backward under each tug while its tip stays at the attachment point, then springs back as the effort releases. The pulse is in the movement itself: a fast pull, a short hold, a small release, then a stronger pull. All motion is frame-driven, including the spring and cursor shape.

Custom content

The button has a 300 × 88 reference-pixel content slot. Supply children to replace its label and arrow with your own content. The surrounding surface is still the elastic button; text remains unstretched as it travels with the material. Use compact content that fits the slot.

<CursorGravity color="#e8ef9b" backgroundColor="#201426" tension={1.2}>
  <span style={{ fontSize: 26 }}>Start creating ↗</span>
</CursorGravity>

The composition's actual right edge is always the extraction boundary, including in portrait and square renders. Element sizes scale with the smaller of width / 1280 and height / 720. A transparent background lets the button appear over an existing scene.

Timing

Reference frames at 30 fps and speed={1}:

FramesMotion
0–10Empty scene
10–39Triangular cursor enters from below
39–48Cursor approaches and catches the right edge
48–70First tug, hold, and recoil
70–94Stronger second tug, hold, and recoil
94–132Final pull; the trailing edge releases and springs inward
132–151Button settles; cursor finishes exiting below
151–180Centered button holds

cursorGravityLength is 151, when the reveal and cursor exit have finished. getCursorGravityDuration({ speed }) includes the final hold and returns frames at 30 fps. Multiply by fps / 30 and round up for another frame rate. Playback duration in seconds is consistent across frame rates. The final button stays in place after the sequence ends.

Props
PropTypeDefaultDescription
children
ReactNodeReplace the label and arrow; fits a 300 × 88 content slot
label
string"Create something"Default button label; long text truncates; ignored with children
tension
number1Elastic neck deformation and recoil, clamped to 0–1.5
cursorColor
string"#fffaf0"Rounded triangular cursor and short attachment thread
color
string"#fffaf0"Elastic button surface
textColor
string"#32153c"Button label and arrow
backgroundColor
string"#a800b7"Full-frame background; accepts transparent
speed
number1Time multiplier; zero or negative freezes the empty initial frame
className
stringClass on the full-frame root