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.

tsx
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.

tsx
<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.

Props9

PropTypeDefault
children
ReactNode—

Replace the label and arrow; fits a 300 × 88 content slot

label
string"Create something"

Default button label; long text truncates; ignored with children

tension
number1

Elastic 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
number1

Time multiplier; zero or negative freezes the empty initial frame

className
string—

Class on the full-frame root

  • childrenReactNode

    Replace the label and arrow; fits a 300 × 88 content slot

  • labelstring

    Default button label; long text truncates; ignored with children

    Default"Create something"

  • tensionnumber

    Elastic neck deformation and recoil, clamped to 0–1.5

    Default1

  • cursorColorstring

    Rounded triangular cursor and short attachment thread

    Default"#fffaf0"

  • colorstring

    Elastic button surface

    Default"#fffaf0"

  • textColorstring

    Button label and arrow

    Default"#32153c"

  • backgroundColorstring

    Full-frame background; accepts transparent

    Default"#a800b7"

  • speednumber

    Time multiplier; zero or negative freezes the empty initial frame

    Default1

  • classNamestring

    Class on the full-frame root