Costumary/gsap-choreography

AI coding instructions for scripted GSAP product demo animations — cursor choreography, timeline orchestration, scene tr

Stars 5 Language Last updated 2026-05-27 Source on GitHub @Costumary

Actual rules from this repo

Path in source repo: .cursor/rules/gsap-choreography.mdc · format: mdc

---
description: Use when building multi-scene GSAP animations for product demos, hero walkthroughs, or scripted UI sequences in React. Covers timeline architecture, cursor choreography, scene transitions, click systems, typed text, and animation coordination across components.
globs:
  - "**/*film*"
  - "**/*demo*"
  - "**/*animation*"
  - "**/*choreograph*"
  - "**/*gsap*"
  - "**/*timeline*"
---

# GSAP Choreography

Build scripted, multi-scene product animations using GSAP timelines in React. Not tweens-101 — this is the orchestration layer: how to coordinate cursor movement, click feedback, scene transitions, typed text, and UI state changes into a single looping timeline that feels like a directed film.

## Architecture

```
film-script.ts          → Scene definitions, cursor paths, data (no GSAP)
film-primitives.tsx      → Static DOM: frame, sidebar, cursor SVG (no GSAP)
film-panels.tsx          → Scene content: each tab's UI (no GSAP)
film-demo.tsx            → Single useGSAP hook: the entire choreography
animation-provider.tsx   → React context: play/pause/restart coordination
```

**The rule:** GSAP code lives in exactly one file. Everything else is inert DOM with `data-film-*` attributes. This separation means designers edit visuals without touching animation, and the timeline can target any element by data attribute.

## Timeline as Central Authority

One `gsap.timeline()` owns the entire sequence. Every animation is a `.to()` or `.set()` call on this timeline, positioned with labels.

```tsx
const timeline = gsap.timeline({
  repeat: -1,
  repeatDelay: 0.75,
  defaults: { ease: "power3.out" },
});

timeline
  .to(cursor, { x: target.x, y: target.y, duration: 0.72 }, "groupMove")
  .to(toolbar, { backgroundColor: "rgba(182,95,114,0.15)", duration: 0.18 }, "groupMove+=0.55");

click(target, "groupClick");

timeline
  .to(groupOutline, { autoAlpha: 1, scale: 1, duration: 0.38 }, "groupClick+=0.1")
  .to(note, { autoAlpha: 1, y: 0, duration: 0.38 }, "noteClick+=0.12");
```

**Why one timeline:** Multiple timelines desync on tab-switch, resize, or replay. A single timeline with labels gives you scrubbing, replay, and pause for free. Labels are the API between "what happens" and "when."

## Label Naming Convention

Name labels by what the user *does*, not what animates:

```
"dropClick"        → user drops references onto board
"groupClick"       → user clicks the group tool
"noteMove"         → cursor travels to the note tool
"sidebarCollapse"  → sidebar shrinks to icon-only
"navMaterials"     → user clicks Materials in nav
"typeStart"        → cursor reaches the input field
"sendClick"        → user clicks send on the chat
"settle"           → animation winds down, UI resets
```

Position everything relative to labels with `"labelName+=0.12"` offsets. Never use absolute seconds.

## Cursor System

The cursor is a positioned SVG that moves via the timeline. It must feel human, not robotic.

### Movement

```tsx
.to(cursor, { x: target.x, y: target.y, duration: 0.72, ease: "power2.out" }, "moveLabel")
```

- Short hop (same area): 0.4–0.6s
- Cross-screen travel: 0.7–1.0s
- Entering from off-screen: 1.0–1.2s

Never use `ease: "linear"` for cursor movement.

### Click Feedback

Every click needs a cursor squeeze and a ripple burst:

```tsx
function click(position: { x: number; y: number }, label: string) {
  timeline
    .set(ripple, { x: position.x, y: position.y, scale: 0.2, autoAlpha: 0.55 }, label)
    .to(cursor, { scale: 0.88, duration: 0.08, ease: "power2.out" }, label)
    .to(ripple, { scale: 3.4, autoAlpha: 0, duration: 0.54, ease: "power2.out" }, label)
    .to(cursor, { scale: 1, duration: 0.16, ease: "back.out(2.2)" }, `${label}+=0.09`);
}
```

### Cursor Position Measurement (CRITICAL)

**Never hardcode cursor coordinates.** Always measure targets from actual DOM elements:

```tsx
const measure = (el: HTMLElement) => {
  const frame = root.getBoundingClientRect();
  const r = el.getBoundingClientRect();
  const s = scale || 1;
  return {
    x: Math.round((r.left + r.width / 2 - frame.left) / s),
    y: Math.round((r.top + r.height / 2 - frame.top) / s),
  };
};
```

## Scene Transitions

```tsx
// 1. Update nav state (immediate, not animated)
.add(() => {
  setActiveNav(root, "nav-materials");
  setProgressDots(root, 1);
  setCaption(captionEl, scenes[1].caption);
}, "navMaterials+=0.56")

// 2. Cross-fade content
.to(prevTab, { autoAlpha: 0, duration: 0.24 }, "materialsClick+=0.08")
.to(nextTab, { autoAlpha: 1, duration: 0.28 }, "materialsClick+=0.14")
```

Use `autoAlpha` (not `opacity`) — it sets `visibility: hidden` at 0.

## Typed Text Effect

```tsx
const prompt = "What should I do next?";
const counter = { x: 0 };

timeline
  .to(placeholder, { autoAlpha: 0, duration: 0.1 }, "typeStart+=0.56")
  .to(caret, { autoAlpha: 1, duration: 0.1 }, "typeStart+=0.56")
  .to(counter, {
    x: prompt.length,
    duration: 1.65,
    ease: "none",
    onUpdate: () => {
      target.textContent = prompt.slice(0, Math.round(counter.x));
    },
  }, "typeStart+=0.62");
```

`ease: "none"` is correct — real typing is constant speed.

## Element Initial States

Set every animated element's start state explicitly at timeline position 0:

```tsx
gsap.set(cursor, { x: offScreenX, y: offScreenY });
gsap.set(cards, { autoAlpha: 0, y: 26, scale: 0.82 });
```

For looping timelines, add a reset block at position 0.

## Stagger Patterns

```tsx
.to(cards, {
  autoAlpha: 1, y: 0, scale: 1,
  rotation: (i: number) => rotations[i],
  stagger: 0.07,
  duration: 0.46,
}, "dropClick+=0.16")
```

0.05–0.08s for related items, 0.10–0.15s for independent items.

## Responsive Scaling

Render at fixed design size, scale with CSS transform:

```tsx
const FILM_WIDTH = 1180;
const FILM_HEIGHT = 760;

<div style={{
  width: FILM_WIDTH,
  height: FILM_HEIGHT,
  transform: `scale(${scale})`,
  transformOrigin: "top left",
}} />
```

## Data Attribute Convention

```
data-film-cursor          → the cursor

Content truncated. View full file in the source repo (linked above).

View raw on GitHub

Why this is listed

This repository appears on Cursor Rules Live because it matches the tracker's GitHub Search criteria (cursor-rules) and was active in the recent indexing window. The tracker refreshes every 15 minutes, so the metadata above reflects the state at the most recent index pass. If the data here looks stale, the source repository may have been archived or moved out of the tracked topic; the next cron tick will reconcile.

Similar in this tracker

Explore by category