@mehshekari/gsap-motion 3.1.1 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,183 +1,183 @@
1
- # Interaction
2
-
3
- Pointer-driven motion. The engineering constraint is that these fire at 60–120Hz,
4
- so the wrong architecture costs a frame budget rather than a few milliseconds.
5
-
6
- Every interaction has three parts, and the third is the one people skip:
7
-
8
- ```text
9
- input → response → recovery
10
- ```
11
-
12
- The recovery is what makes it feel physical. A button that springs toward the
13
- cursor and snaps back on leave feels broken; the same button easing back over
14
- 0.6s feels alive.
15
-
16
- ## The rule: `quickTo`, not a tween per event
17
-
18
- ```ts
19
- // Wrong — allocates a tween per pointer event, and they fight each other
20
- onMouseMove = (e) => gsap.to(el, { x: e.clientX, duration: 0.5 });
21
-
22
- // Right — one tween, retargeted
23
- const moveX = gsap.quickTo(el, "x", { duration: 0.5, ease: "power3" });
24
- const moveY = gsap.quickTo(el, "y", { duration: 0.5, ease: "power3" });
25
- onMouseMove = (e) => { moveX(e.clientX); moveY(e.clientY); };
26
- ```
27
-
28
- Create the `quickTo` functions once, inside the `useGSAP` body. This is the
29
- single most important performance decision in pointer work.
30
-
31
- ## Magnetic element
32
-
33
- The element leans toward the cursor while it is near, and returns when it
34
- leaves.
35
-
36
- ```ts
37
- const xTo = gsap.quickTo(el, "x", { duration: 0.6, ease: "power3" });
38
- const yTo = gsap.quickTo(el, "y", { duration: 0.6, ease: "power3" });
39
-
40
- const onMove = (e: PointerEvent) => {
41
- const r = el.getBoundingClientRect();
42
- xTo((e.clientX - (r.left + r.width / 2)) * 0.35);
43
- yTo((e.clientY - (r.top + r.height / 2)) * 0.35);
44
- };
45
-
46
- const onLeave = () => { xTo(0); yTo(0); };
47
- ```
48
-
49
- - **Strength 0.2–0.4.** Above that the element outruns the cursor and feels
50
- unmoored.
51
- - **Measure in the handler, not once.** Scroll and layout change the rect. If
52
- that shows up as cost, cache it on enter and invalidate on scroll.
53
- - **The hit area should be larger than the element** — a padded wrapper listens,
54
- the inner element moves. That is what makes the pull feel like a field rather
55
- than a hover state.
56
-
57
- ## Cursor follower
58
-
59
- ```ts
60
- const xTo = gsap.quickTo(dot, "x", { duration: 0.35, ease: "power3" });
61
- const yTo = gsap.quickTo(dot, "y", { duration: 0.35, ease: "power3" });
62
-
63
- window.addEventListener("pointermove", (e) => { xTo(e.clientX); yTo(e.clientY); });
64
- ```
65
-
66
- - Two layers at different durations (0.1 and 0.5) give the trailing-ring look
67
- without a trail of elements.
68
- - `pointer-events: none` on the follower, always.
69
- - **Only on devices that have a cursor.** Gate on
70
- `(hover: hover) and (pointer: fine)` — a follower on a touch device is dead
71
- weight and sits in the corner.
72
- - Hiding the native cursor is a real accessibility cost. Do it only if the
73
- replacement is at least as legible over every surface, and never over text
74
- inputs.
75
-
76
- ## Observer
77
-
78
- One unified handler for wheel, touch, and pointer — the right tool when you want
79
- a gesture rather than a scroll position.
80
-
81
- ```ts
82
- import { Observer } from "gsap/Observer";
83
- gsap.registerPlugin(Observer);
84
-
85
- const obs = Observer.create({
86
- target: window,
87
- type: "wheel,touch,pointer",
88
- onUp: () => next(),
89
- onDown: () => prev(),
90
- tolerance: 10,
91
- preventDefault: true,
92
- });
93
-
94
- obs.kill(); // only needed outside a useGSAP body — see react-nextjs.md
95
- ```
96
-
97
- This is how full-screen section-snapping and swipe carousels are built without
98
- ScrollTrigger. `preventDefault: true` takes over native scrolling — a large
99
- commitment, and it must not trap a keyboard or screen-reader user.
100
-
101
- ## Draggable and Inertia
102
-
103
- ```ts
104
- import { Draggable } from "gsap/Draggable";
105
- import { InertiaPlugin } from "gsap/InertiaPlugin";
106
- gsap.registerPlugin(Draggable, InertiaPlugin);
107
-
108
- Draggable.create(el, {
109
- type: "x",
110
- bounds: container,
111
- inertia: true,
112
- snap: { x: (v) => Math.round(v / 320) * 320 },
113
- onDragEnd() { /* ... */ },
114
- });
115
- ```
116
-
117
- `inertia: true` gives the flick-and-settle that makes a drag feel native.
118
- `Draggable.create` returns instances — `.kill()` them on unmount.
119
-
120
- Anything draggable needs a keyboard path to the same outcome. A carousel that
121
- only responds to drag is unusable without a pointer.
122
-
123
- ## Hover with a timeline
124
-
125
- Build once, play and reverse. Better than creating tweens per event, and the
126
- return matches the arrival for free.
127
-
128
- ```ts
129
- const hover = gsap.timeline({ paused: true })
130
- .to(el, { scale: 1.04, duration: 0.3, ease: "power2.out" })
131
- .to(label, { y: -2, duration: 0.3 }, "<");
132
-
133
- onEnter = () => hover.play();
134
- onLeave = () => hover.reverse();
135
- ```
136
-
137
- In React, if the timeline is built inside `useGSAP` and only *played* from the
138
- handler, no `contextSafe` is needed — see [react-nextjs.md](react-nextjs.md).
139
-
140
- ## `overwrite: "auto"`
141
-
142
- The classic bug: enter, leave, enter fast, and the element sits halfway.
143
- Whatever retriggers needs `overwrite: "auto"` so the conflicting tween is killed
144
- rather than blended.
145
-
146
- ## Spotlight
147
-
148
- A pointer-tracked gradient. Drive CSS variables rather than re-rendering:
149
-
150
- ```ts
151
- const setX = gsap.quickSetter(card, "--spot-x", "px");
152
- const setY = gsap.quickSetter(card, "--spot-y", "px");
153
- ```
154
-
155
- ```css
156
- background: radial-gradient(
157
- 240px at var(--spot-x) var(--spot-y),
158
- var(--glow), transparent 70%
159
- );
160
- ```
161
-
162
- Cheap, and the styling stays in CSS where it can follow the theme.
163
-
164
- ## Accessibility
165
-
166
- - **Everything reachable by pointer must be reachable by keyboard.** Hover state
167
- and focus state should be the same visual.
168
- - Never destroy the focus ring with a transform that moves the element out from
169
- under it.
170
- - Gate hover effects on `(hover: hover)`. On touch, `:hover` sticks after a tap.
171
- - Under `prefers-reduced-motion: reduce`, keep the state change and drop the
172
- travel — see [accessibility.md](accessibility.md).
173
-
174
- ## Failures
175
-
176
- | Symptom | Cause |
177
- |---|---|
178
- | Janky on move | a tween per event, or React state in the handler |
179
- | Element sticks halfway | missing `overwrite: "auto"` |
180
- | Magnetic pull feels wrong | strength too high, or rect measured once |
181
- | Follower stuck in a corner | mounted on a touch device |
182
- | Hover sticks after tap | no `(hover: hover)` gate |
183
- | Effects survive navigation | `Observer`/`Draggable` not killed, listeners not removed |
1
+ # Interaction
2
+
3
+ Pointer-driven motion. The engineering constraint is that these fire at 60–120Hz,
4
+ so the wrong architecture costs a frame budget rather than a few milliseconds.
5
+
6
+ Every interaction has three parts, and the third is the one people skip:
7
+
8
+ ```text
9
+ input → response → recovery
10
+ ```
11
+
12
+ The recovery is what makes it feel physical. A button that springs toward the
13
+ cursor and snaps back on leave feels broken; the same button easing back over
14
+ 0.6s feels alive.
15
+
16
+ ## The rule: `quickTo`, not a tween per event
17
+
18
+ ```ts
19
+ // Wrong — allocates a tween per pointer event, and they fight each other
20
+ onMouseMove = (e) => gsap.to(el, { x: e.clientX, duration: 0.5 });
21
+
22
+ // Right — one tween, retargeted
23
+ const moveX = gsap.quickTo(el, "x", { duration: 0.5, ease: "power3" });
24
+ const moveY = gsap.quickTo(el, "y", { duration: 0.5, ease: "power3" });
25
+ onMouseMove = (e) => { moveX(e.clientX); moveY(e.clientY); };
26
+ ```
27
+
28
+ Create the `quickTo` functions once, inside the `useGSAP` body. This is the
29
+ single most important performance decision in pointer work.
30
+
31
+ ## Magnetic element
32
+
33
+ The element leans toward the cursor while it is near, and returns when it
34
+ leaves.
35
+
36
+ ```ts
37
+ const xTo = gsap.quickTo(el, "x", { duration: 0.6, ease: "power3" });
38
+ const yTo = gsap.quickTo(el, "y", { duration: 0.6, ease: "power3" });
39
+
40
+ const onMove = (e: PointerEvent) => {
41
+ const r = el.getBoundingClientRect();
42
+ xTo((e.clientX - (r.left + r.width / 2)) * 0.35);
43
+ yTo((e.clientY - (r.top + r.height / 2)) * 0.35);
44
+ };
45
+
46
+ const onLeave = () => { xTo(0); yTo(0); };
47
+ ```
48
+
49
+ - **Strength 0.2–0.4.** Above that the element outruns the cursor and feels
50
+ unmoored.
51
+ - **Measure in the handler, not once.** Scroll and layout change the rect. If
52
+ that shows up as cost, cache it on enter and invalidate on scroll.
53
+ - **The hit area should be larger than the element** — a padded wrapper listens,
54
+ the inner element moves. That is what makes the pull feel like a field rather
55
+ than a hover state.
56
+
57
+ ## Cursor follower
58
+
59
+ ```ts
60
+ const xTo = gsap.quickTo(dot, "x", { duration: 0.35, ease: "power3" });
61
+ const yTo = gsap.quickTo(dot, "y", { duration: 0.35, ease: "power3" });
62
+
63
+ window.addEventListener("pointermove", (e) => { xTo(e.clientX); yTo(e.clientY); });
64
+ ```
65
+
66
+ - Two layers at different durations (0.1 and 0.5) give the trailing-ring look
67
+ without a trail of elements.
68
+ - `pointer-events: none` on the follower, always.
69
+ - **Only on devices that have a cursor.** Gate on
70
+ `(hover: hover) and (pointer: fine)` — a follower on a touch device is dead
71
+ weight and sits in the corner.
72
+ - Hiding the native cursor is a real accessibility cost. Do it only if the
73
+ replacement is at least as legible over every surface, and never over text
74
+ inputs.
75
+
76
+ ## Observer
77
+
78
+ One unified handler for wheel, touch, and pointer — the right tool when you want
79
+ a gesture rather than a scroll position.
80
+
81
+ ```ts
82
+ import { Observer } from "gsap/Observer";
83
+ gsap.registerPlugin(Observer);
84
+
85
+ const obs = Observer.create({
86
+ target: window,
87
+ type: "wheel,touch,pointer",
88
+ onUp: () => next(),
89
+ onDown: () => prev(),
90
+ tolerance: 10,
91
+ preventDefault: true,
92
+ });
93
+
94
+ obs.kill(); // only needed outside a useGSAP body — see adapter/react.md
95
+ ```
96
+
97
+ This is how full-screen section-snapping and swipe carousels are built without
98
+ ScrollTrigger. `preventDefault: true` takes over native scrolling — a large
99
+ commitment, and it must not trap a keyboard or screen-reader user.
100
+
101
+ ## Draggable and Inertia
102
+
103
+ ```ts
104
+ import { Draggable } from "gsap/Draggable";
105
+ import { InertiaPlugin } from "gsap/InertiaPlugin";
106
+ gsap.registerPlugin(Draggable, InertiaPlugin);
107
+
108
+ Draggable.create(el, {
109
+ type: "x",
110
+ bounds: container,
111
+ inertia: true,
112
+ snap: { x: (v) => Math.round(v / 320) * 320 },
113
+ onDragEnd() { /* ... */ },
114
+ });
115
+ ```
116
+
117
+ `inertia: true` gives the flick-and-settle that makes a drag feel native.
118
+ `Draggable.create` returns instances — `.kill()` them on unmount.
119
+
120
+ Anything draggable needs a keyboard path to the same outcome. A carousel that
121
+ only responds to drag is unusable without a pointer.
122
+
123
+ ## Hover with a timeline
124
+
125
+ Build once, play and reverse. Better than creating tweens per event, and the
126
+ return matches the arrival for free.
127
+
128
+ ```ts
129
+ const hover = gsap.timeline({ paused: true })
130
+ .to(el, { scale: 1.04, duration: 0.3, ease: "power2.out" })
131
+ .to(label, { y: -2, duration: 0.3 }, "<");
132
+
133
+ onEnter = () => hover.play();
134
+ onLeave = () => hover.reverse();
135
+ ```
136
+
137
+ In React, if the timeline is built inside `useGSAP` and only *played* from the
138
+ handler, no `contextSafe` is needed — see [react.md](../adapter/react.md).
139
+
140
+ ## `overwrite: "auto"`
141
+
142
+ The classic bug: enter, leave, enter fast, and the element sits halfway.
143
+ Whatever retriggers needs `overwrite: "auto"` so the conflicting tween is killed
144
+ rather than blended.
145
+
146
+ ## Spotlight
147
+
148
+ A pointer-tracked gradient. Drive CSS variables rather than re-rendering:
149
+
150
+ ```ts
151
+ const setX = gsap.quickSetter(card, "--spot-x", "px");
152
+ const setY = gsap.quickSetter(card, "--spot-y", "px");
153
+ ```
154
+
155
+ ```css
156
+ background: radial-gradient(
157
+ 240px at var(--spot-x) var(--spot-y),
158
+ var(--glow), transparent 70%
159
+ );
160
+ ```
161
+
162
+ Cheap, and the styling stays in CSS where it can follow the theme.
163
+
164
+ ## Accessibility
165
+
166
+ - **Everything reachable by pointer must be reachable by keyboard.** Hover state
167
+ and focus state should be the same visual.
168
+ - Never destroy the focus ring with a transform that moves the element out from
169
+ under it.
170
+ - Gate hover effects on `(hover: hover)`. On touch, `:hover` sticks after a tap.
171
+ - Under `prefers-reduced-motion: reduce`, keep the state change and drop the
172
+ travel — see [accessibility.md](accessibility.md).
173
+
174
+ ## Failures
175
+
176
+ | Symptom | Cause |
177
+ |---|---|
178
+ | Janky on move | a tween per event, or React state in the handler |
179
+ | Element sticks halfway | missing `overwrite: "auto"` |
180
+ | Magnetic pull feels wrong | strength too high, or rect measured once |
181
+ | Follower stuck in a corner | mounted on a touch device |
182
+ | Hover sticks after tap | no `(hover: hover)` gate |
183
+ | Effects survive navigation | `Observer`/`Draggable` not killed, listeners not removed |
@@ -1,119 +1,119 @@
1
- # Routing
2
-
3
- What to load, and what to do when the request is a feeling rather than a
4
- command.
5
-
6
- ## No argument
7
-
8
- Do not guess a target and do not invent an animation. Ask what should move, and
9
- offer the menu below — grouped, so the question is easy to answer.
10
-
11
- > What are we animating?
12
- >
13
- > - **Entrance** — something appearing: a hero, a section, a list, an image
14
- > - **Scroll** — something driven by scroll position: a sequence, a pin, a
15
- > horizontal run
16
- > - **Interaction** — something answering the pointer: a button, a cursor, a
17
- > card
18
- > - **Continuous** — something always moving: a marquee, an orbit, ambient drift
19
- > - **Transition** — moving between two states: a route, a modal, a layout
20
- > - **Loading** — a loader, a progress indicator, a site intro
21
- > - **Existing** — something already animated that feels wrong, costs too much,
22
- > or should go
23
-
24
- ## Classifying a description
25
-
26
- Most requests are a feeling, not a command. Classify, then route.
27
-
28
- | What they say | What they mean | Route to |
29
- |---|---|---|
30
- | "make it cinematic" | slow, weighted, sequenced, one thing at a time | `intro` · [cinematic](../preset/cinematic.md) |
31
- | "make it feel premium" | fewer moving parts, longer eases, no bounce | `tune` · [motion-design](motion-design.md) |
32
- | "make it feel alive" | ambient continuous motion under static content | `ambient` · [marquee](../preset/marquee.md) |
33
- | "make it pop / bolder" | more contrast in timing, not more elements moving | `tune` · [motion-design](motion-design.md) |
34
- | "like Apple" | scroll-scrubbed, pinned, one idea per screen | `scroll` · [scroll-storytelling](../preset/scroll-storytelling.md) |
35
- | "futuristic / technical" | scramble, draw-on, mechanical easing, grid reveals | `text` + `svg` |
36
- | "liquid / organic" | morph, path motion, overlapping soft easing | `svg` · [loader](../preset/loader.md) |
37
- | "something crazy" | layered techniques with one clear concept | see below |
38
- | "smooth image reveal" | clip-path or mask, not opacity | [reveal](../preset/reveal.md) |
39
- | "the hero is boring" | hierarchy problem before a motion problem | `tune`, then `reveal` |
40
- | "it feels janky" | cost or trigger problem | `audit` · [performance](performance.md) |
41
-
42
- ### "Something crazy"
43
-
44
- A fade-and-scale is a failure of nerve; twelve techniques at once is a failure
45
- of taste. Pick **one concept** and let three techniques serve it. The concept is
46
- the thing you can say in a sentence — "the logo writes itself, then the dot that
47
- drew it runs off and writes the word" — and every technique either serves that
48
- sentence or is cut.
49
-
50
- ## Load map
51
-
52
- The project's own rules come first, then the size tiers in SKILL.md, setup step
53
- 3: a one-element change loads [motion-design](motion-design.md) and the
54
- command's own reference only. The table applies from component size up — a
55
- single hover takes [interaction](interaction.md) and nothing else from its row.
56
-
57
- | Request | Add |
58
- |---|---|
59
- | SVG draw / morph / path | [svg](svg.md), [timeline](timeline.md) |
60
- | Scroll sequence, pin, scrub | [scrolltrigger](scrolltrigger.md), [timeline](timeline.md), [performance](performance.md) |
61
- | Horizontal scroll | [scrolltrigger](scrolltrigger.md), [scroll-storytelling](../preset/scroll-storytelling.md) |
62
- | Text reveal, kinetic type | [text](text.md), [timeline](timeline.md) |
63
- | Magnetic, cursor, hover | [interaction](interaction.md), [performance](performance.md) |
64
- | Drag, flick, inertia | [interaction](interaction.md) |
65
- | Layout / shared element / modal | [flip](flip.md) |
66
- | Page or route transition | [flip](flip.md), [page-transition](../preset/page-transition.md), [react-nextjs](react-nextjs.md) or [frameworks](frameworks.md) |
67
- | Loader, splash, intro | [svg](svg.md), [timeline](timeline.md), [accessibility](accessibility.md) |
68
- | Marquee, orbit, ambient | [timeline](timeline.md), [performance](performance.md) |
69
- | Three.js / R3F | [three-r3f](three-r3f.md), [performance](performance.md) |
70
- | Audit an existing animation | [performance](performance.md), [accessibility](accessibility.md), [react-nextjs](react-nextjs.md) or [frameworks](frameworks.md) |
71
- | Retime / re-ease | [motion-design](motion-design.md) only |
72
-
73
- Do not load a domain file "for completeness". Loading `three-r3f.md` for a
74
- button hover is the failure this map exists to prevent.
75
-
76
- ## `audit`
77
-
78
- **Run the checker first.** It is deterministic, it costs a second, and it finds
79
- the mechanical failures before you spend attention on them:
80
-
81
- ```bash
82
- node .claude/skills/gsap-creative-animation/scripts/audit-gsap.mjs <path>
83
- ```
84
-
85
- **When an animation is not visible, ask whether it was ever built.** Before
86
- touching timings or transforms, check that the code which creates it actually
87
- runs. Animation is routinely built behind a condition — an image decode race, a
88
- `ready.then()`, a ScrollTrigger that has not fired, a `matchMedia` branch that
89
- does not match, an early `return` on a missing ref — and a tween that was never
90
- created looks exactly like one that is mis-timed. A typical case: an image wipe
91
- added only once the image decodes inside a short budget. On a first visit the
92
- image is never cached, the branch never runs, and every hypothesis about the
93
- wipe's geometry is wasted effort.
94
-
95
- Then read for what it cannot see. Check in this order — the first two are where
96
- real bugs live:
97
-
98
- 1. **Lifecycle** — is every animation inside a `useGSAP`/`gsap.context` scope?
99
- Are ScrollTriggers killed? Are listeners removed? Does a `matchMedia` get
100
- reverted? See [react-nextjs](react-nextjs.md).
101
- 2. **Cost** — layout-triggering properties, per-frame React state, filters on
102
- large areas, one ScrollTrigger per item where one would do. See
103
- [performance](performance.md).
104
- 3. **Reduced motion** — is there a branch, and does it still communicate?
105
- 4. **Craft** — rhythm, easing on loops, contrast. See [motion-design](motion-design.md).
106
-
107
- Report findings most-severe first, with the file and line. Do not rewrite
108
- unless asked.
109
-
110
- ## `strip`
111
-
112
- Removing motion is a legitimate outcome and often the right one. Cut an
113
- animation when it answers none of hierarchy, causality, continuity, state, or
114
- personality; when it delays content the visitor came for; when it repeats on
115
- every route and stopped being noticed on the second one; or when it exists
116
- because a plugin was available.
117
-
118
- Say what you removed and what it was costing. Do not replace it with a smaller
119
- animation unless the user asks.
1
+ # Routing
2
+
3
+ What to load, and what to do when the request is a feeling rather than a
4
+ command.
5
+
6
+ ## No argument
7
+
8
+ Do not guess a target and do not invent an animation. Ask what should move, and
9
+ offer the menu below — grouped, so the question is easy to answer.
10
+
11
+ > What are we animating?
12
+ >
13
+ > - **Entrance** — something appearing: a hero, a section, a list, an image
14
+ > - **Scroll** — something driven by scroll position: a sequence, a pin, a
15
+ > horizontal run
16
+ > - **Interaction** — something answering the pointer: a button, a cursor, a
17
+ > card
18
+ > - **Continuous** — something always moving: a marquee, an orbit, ambient drift
19
+ > - **Transition** — moving between two states: a route, a modal, a layout
20
+ > - **Loading** — a loader, a progress indicator, a site intro
21
+ > - **Existing** — something already animated that feels wrong, costs too much,
22
+ > or should go
23
+
24
+ ## Classifying a description
25
+
26
+ Most requests are a feeling, not a command. Classify, then route.
27
+
28
+ | What they say | What they mean | Route to |
29
+ |---|---|---|
30
+ | "make it cinematic" | slow, weighted, sequenced, one thing at a time | `intro` · [cinematic](../preset/cinematic.md) |
31
+ | "make it feel premium" | fewer moving parts, longer eases, no bounce | `tune` · [motion-design](motion-design.md) |
32
+ | "make it feel alive" | ambient continuous motion under static content | `ambient` · [marquee](../preset/marquee.md) |
33
+ | "make it pop / bolder" | more contrast in timing, not more elements moving | `tune` · [motion-design](motion-design.md) |
34
+ | "like Apple" | scroll-scrubbed, pinned, one idea per screen | `scroll` · [scroll-storytelling](../preset/scroll-storytelling.md) |
35
+ | "futuristic / technical" | scramble, draw-on, mechanical easing, grid reveals | `text` + `svg` |
36
+ | "liquid / organic" | morph, path motion, overlapping soft easing | `svg` · [loader](../preset/loader.md) |
37
+ | "something crazy" | layered techniques with one clear concept | see below |
38
+ | "smooth image reveal" | clip-path or mask, not opacity | [reveal](../preset/reveal.md) |
39
+ | "the hero is boring" | hierarchy problem before a motion problem | `tune`, then `reveal` |
40
+ | "it feels janky" | cost or trigger problem | `audit` · [performance](performance.md) |
41
+
42
+ ### "Something crazy"
43
+
44
+ A fade-and-scale is a failure of nerve; twelve techniques at once is a failure
45
+ of taste. Pick **one concept** and let three techniques serve it. The concept is
46
+ the thing you can say in a sentence — "the logo writes itself, then the dot that
47
+ drew it runs off and writes the word" — and every technique either serves that
48
+ sentence or is cut.
49
+
50
+ ## Load map
51
+
52
+ The project's own rules come first, then the size tiers in SKILL.md, setup step
53
+ 3: a one-element change loads [motion-design](motion-design.md) and the
54
+ command's own reference only. The table applies from component size up — a
55
+ single hover takes [interaction](interaction.md) and nothing else from its row.
56
+
57
+ | Request | Add |
58
+ |---|---|
59
+ | SVG draw / morph / path | [svg](svg.md), [timeline](timeline.md) |
60
+ | Scroll sequence, pin, scrub | [scrolltrigger](scrolltrigger.md), [timeline](timeline.md), [performance](performance.md) |
61
+ | Horizontal scroll | [scrolltrigger](scrolltrigger.md), [scroll-storytelling](../preset/scroll-storytelling.md) |
62
+ | Text reveal, kinetic type | [text](text.md), [timeline](timeline.md) |
63
+ | Magnetic, cursor, hover | [interaction](interaction.md), [performance](performance.md) |
64
+ | Drag, flick, inertia | [interaction](interaction.md) |
65
+ | Layout / shared element / modal | [flip](flip.md) |
66
+ | Page or route transition | [flip](flip.md), [page-transition](../preset/page-transition.md), this project's adapter |
67
+ | Loader, splash, intro | [svg](svg.md), [timeline](timeline.md), [accessibility](accessibility.md) |
68
+ | Marquee, orbit, ambient | [timeline](timeline.md), [performance](performance.md) |
69
+ | Three.js / R3F | [three](../adapter/three.md) or [r3f](../adapter/r3f.md), [performance](performance.md) |
70
+ | Audit an existing animation | [performance](performance.md), [accessibility](accessibility.md), this project's adapter |
71
+ | Retime / re-ease | [motion-design](motion-design.md) only |
72
+
73
+ Do not load a domain file "for completeness". Loading `adapter/three.md` for a
74
+ button hover is the failure this map exists to prevent.
75
+
76
+ ## `audit`
77
+
78
+ **Run the checker first.** It is deterministic, it costs a second, and it finds
79
+ the mechanical failures before you spend attention on them:
80
+
81
+ ```bash
82
+ node .claude/skills/gsap-creative-animation/scripts/audit-gsap.mjs <path>
83
+ ```
84
+
85
+ **When an animation is not visible, ask whether it was ever built.** Before
86
+ touching timings or transforms, check that the code which creates it actually
87
+ runs. Animation is routinely built behind a condition — an image decode race, a
88
+ `ready.then()`, a ScrollTrigger that has not fired, a `matchMedia` branch that
89
+ does not match, an early `return` on a missing ref — and a tween that was never
90
+ created looks exactly like one that is mis-timed. A typical case: an image wipe
91
+ added only once the image decodes inside a short budget. On a first visit the
92
+ image is never cached, the branch never runs, and every hypothesis about the
93
+ wipe's geometry is wasted effort.
94
+
95
+ Then read for what it cannot see. Check in this order — the first two are where
96
+ real bugs live:
97
+
98
+ 1. **Lifecycle** — is every animation inside a `useGSAP`/`gsap.context` scope?
99
+ Are ScrollTriggers killed? Are listeners removed? Does a `matchMedia` get
100
+ reverted? See this project's adapter.
101
+ 2. **Cost** — layout-triggering properties, per-frame React state, filters on
102
+ large areas, one ScrollTrigger per item where one would do. See
103
+ [performance](performance.md).
104
+ 3. **Reduced motion** — is there a branch, and does it still communicate?
105
+ 4. **Craft** — rhythm, easing on loops, contrast. See [motion-design](motion-design.md).
106
+
107
+ Report findings most-severe first, with the file and line. Do not rewrite
108
+ unless asked.
109
+
110
+ ## `strip`
111
+
112
+ Removing motion is a legitimate outcome and often the right one. Cut an
113
+ animation when it answers none of hierarchy, causality, continuity, state, or
114
+ personality; when it delays content the visitor came for; when it repeats on
115
+ every route and stopped being noticed on the second one; or when it exists
116
+ because a plugin was available.
117
+
118
+ Say what you removed and what it was costing. Do not replace it with a smaller
119
+ animation unless the user asks.
@@ -176,7 +176,7 @@ useGSAP(() => {
176
176
 
177
177
  After a route change in the App Router, the new page's layout is measured before
178
178
  images settle. `ScrollTrigger.refresh()` on mount is usually needed. See
179
- [react-nextjs.md](react-nextjs.md).
179
+ [react.md](../adapter/react.md).
180
180
 
181
181
  ## Failures
182
182
 
@@ -24,7 +24,7 @@ as JSX.
24
24
 
25
25
  Without them you end up at `querySelectorAll("path")[3]`, which breaks on the
26
26
  next export. Prefix them, and namespace per instance — see
27
- [react-nextjs.md](react-nextjs.md). For a set animated together, one
27
+ [react.md](../adapter/react.md). For a set animated together, one
28
28
  `data-*` attribute beats a run of ids:
29
29
 
30
30
  ```tsx
@@ -103,7 +103,7 @@ onMouseLeave = () => hover.reverse();
103
103
 
104
104
  `reverse()` uses the same easing backwards, so the return matches the arrival
105
105
  without a second definition. In React, anything built outside the `useGSAP`
106
- body must go through `contextSafe` — see [react-nextjs.md](react-nextjs.md).
106
+ body must go through `contextSafe` — see [react.md](../adapter/react.md).
107
107
 
108
108
  ## Holds
109
109