@mehshekari/gsap-motion 2.1.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.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +24 -0
  3. package/README.md +59 -0
  4. package/bin/gsap-motion.mjs +304 -0
  5. package/package.json +48 -0
  6. package/skill/LICENSE +21 -0
  7. package/skill/NOTICE.md +24 -0
  8. package/skill/SKILL.md +181 -0
  9. package/skill/example/cinematic-intro.md +202 -0
  10. package/skill/example/magnetic-button.md +101 -0
  11. package/skill/example/svg-loader.md +180 -0
  12. package/skill/preset/card-stack.md +175 -0
  13. package/skill/preset/cinematic.md +140 -0
  14. package/skill/preset/cursor.md +184 -0
  15. package/skill/preset/loader.md +172 -0
  16. package/skill/preset/magnetic.md +155 -0
  17. package/skill/preset/marquee.md +177 -0
  18. package/skill/preset/page-transition.md +151 -0
  19. package/skill/preset/reveal.md +146 -0
  20. package/skill/preset/scroll-storytelling.md +176 -0
  21. package/skill/reference/accessibility.md +143 -0
  22. package/skill/reference/core-gsap.md +233 -0
  23. package/skill/reference/flip.md +220 -0
  24. package/skill/reference/frameworks.md +153 -0
  25. package/skill/reference/interaction.md +183 -0
  26. package/skill/reference/motion-design.md +144 -0
  27. package/skill/reference/performance.md +216 -0
  28. package/skill/reference/project-rules.md +178 -0
  29. package/skill/reference/react-nextjs.md +234 -0
  30. package/skill/reference/routing.md +119 -0
  31. package/skill/reference/scrolltrigger.md +191 -0
  32. package/skill/reference/svg.md +201 -0
  33. package/skill/reference/text.md +150 -0
  34. package/skill/reference/three-r3f.md +161 -0
  35. package/skill/reference/timeline.md +147 -0
  36. package/skill/scripts/audit-gsap.mjs +157 -0
  37. package/skill/scripts/audit-svg.mjs +358 -0
  38. package/skill/scripts/lib/rules.mjs +555 -0
  39. package/skill/scripts/lib/source.mjs +238 -0
  40. package/skill/template/useScrollScene.ts +126 -0
  41. package/skill/template/useTimeline.ts +110 -0
@@ -0,0 +1,176 @@
1
+ # Scroll storytelling
2
+
3
+ A pinned section where scrolling drives a sequence. The "Apple product page"
4
+ pattern. Expensive in build time and in the visitor's attention, so it has to
5
+ be carrying a story that genuinely has an order.
6
+
7
+ Mechanics are in [scrolltrigger.md](../reference/scrolltrigger.md); this is how
8
+ to compose one.
9
+
10
+ ## Decide the beats first
11
+
12
+ Write them out before any code. Each beat is one idea, and scroll distance is
13
+ your pacing:
14
+
15
+ ```text
16
+ 0.0 – 0.2 the product, whole
17
+ 0.2 – 0.45 it opens; the first claim arrives
18
+ 0.45 – 0.7 detail view; the second claim
19
+ 0.7 – 1.0 it closes; the CTA
20
+ ```
21
+
22
+ Roughly 600–1000px of scroll per beat. Less and it flicks past; more and the
23
+ visitor thinks the page is stuck.
24
+
25
+ ## One timeline, one trigger
26
+
27
+ Every element in the sequence belongs to the same timeline. Separate triggers
28
+ drift apart the moment you retime anything.
29
+
30
+ ```ts
31
+ const tl = gsap.timeline({
32
+ defaults: { ease: "none" }, // scrubbed — the scrollbar owns the feel
33
+ scrollTrigger: {
34
+ trigger: section,
35
+ start: "top top",
36
+ end: "+=3000",
37
+ pin: true,
38
+ scrub: 1,
39
+ anticipatePin: 1,
40
+ invalidateOnRefresh: true,
41
+ markers: process.env.NODE_ENV === "development",
42
+ },
43
+ });
44
+
45
+ tl.addLabel("open")
46
+ .to("[data-lid]", { rotationX: -110, duration: 1 })
47
+ .from("[data-claim-1]", { autoAlpha: 0, y: 20, duration: 0.4 }, "open+=0.4")
48
+
49
+ .addLabel("detail")
50
+ .to("[data-claim-1]", { autoAlpha: 0, duration: 0.3 }, "detail")
51
+ .to("[data-product]", { scale: 1.6, x: "-20%", duration: 1 }, "detail")
52
+ .from("[data-claim-2]", { autoAlpha: 0, y: 20, duration: 0.4 }, "detail+=0.4");
53
+ ```
54
+
55
+ `ease: "none"` on every child. Easing inside a scrubbed timeline fights the
56
+ visitor's own scrolling and reads as lag — `scrub: 1` is where the smoothing
57
+ belongs.
58
+
59
+ ## Horizontal
60
+
61
+ ```ts
62
+ const panels = gsap.utils.toArray<HTMLElement>("[data-panel]");
63
+
64
+ gsap.to(panels, {
65
+ xPercent: -100 * (panels.length - 1),
66
+ ease: "none",
67
+ scrollTrigger: {
68
+ trigger: track,
69
+ pin: true,
70
+ scrub: 1,
71
+ end: () => `+=${track.offsetWidth}`,
72
+ snap: { snapTo: 1 / (panels.length - 1), duration: 0.4 },
73
+ invalidateOnRefresh: true,
74
+ },
75
+ });
76
+ ```
77
+
78
+ `end` as a function is re-evaluated on refresh, which keeps it correct after a
79
+ resize. `xPercent` avoids measuring widths.
80
+
81
+ In RTL the direction inverts — `+100 *` rather than `-100 *`. Read `dir` from
82
+ the document; see [project-rules.md](../reference/project-rules.md).
83
+
84
+ Horizontal scroll costs the visitor their scroll intuition. Justify it with
85
+ content that is genuinely a sequence.
86
+
87
+ ## Snapping
88
+
89
+ ```ts
90
+ snap: { snapTo: "labels", duration: 0.4, ease: "power2.inOut", delay: 0.1 }
91
+ ```
92
+
93
+ `"labels"` snaps to the timeline's own labels, which is why naming the beats
94
+ pays off twice. Snapping suits discrete steps and fights continuous motion —
95
+ do not snap a scrubbed camera move.
96
+
97
+ ## Progress
98
+
99
+ A scrubbed indicator is cheap and orients the visitor inside a long pin:
100
+
101
+ ```ts
102
+ .to("[data-progress]", { scaleX: 1, transformOrigin: "0% 50%" }, 0);
103
+ ```
104
+
105
+ Placed at position `0` with the timeline's full duration, it tracks overall
106
+ progress regardless of what else is happening.
107
+
108
+ ## Mobile
109
+
110
+ **Do not scale the desktop sequence down.** A 3000px pin on a phone is a long
111
+ time to be stuck, and pinning fights the address bar collapsing.
112
+
113
+ ```ts
114
+ mm.add({ desktop: "(min-width: 768px)", mobile: "(max-width: 767px)" }, (c) => {
115
+ if (c.conditions!.mobile) {
116
+ // Each beat becomes its own in-view reveal. Same content, no pin.
117
+ gsap.utils.toArray("[data-beat]").forEach((beat) =>
118
+ gsap.from(beat as HTMLElement, {
119
+ autoAlpha: 0, y: 24,
120
+ scrollTrigger: { trigger: beat as HTMLElement, start: "top 75%" },
121
+ }),
122
+ );
123
+ return;
124
+ }
125
+ // the pinned sequence
126
+ });
127
+ ```
128
+
129
+ This is the single most important decision in this preset. A different strategy,
130
+ not a smaller one.
131
+
132
+ ## Reduced motion
133
+
134
+ Drop the pin and the scrub. Each beat appears on entry:
135
+
136
+ ```ts
137
+ if (reduced) {
138
+ gsap.set("[data-claim-1], [data-claim-2]", { autoAlpha: 1, y: 0 });
139
+ return;
140
+ }
141
+ ```
142
+
143
+ Scroll-scrubbed pinning is a textbook vestibular trigger — the content moves
144
+ while the page does not. The content must still be reachable and readable.
145
+
146
+ ## Image sequences
147
+
148
+ Frame-by-frame scrubbing looks expensive because it is. Before committing:
149
+ 30–120 frames at a sensible resolution, preloaded, drawn to a `<canvas>` — not
150
+ 120 `<img>` elements.
151
+
152
+ ```ts
153
+ const frame = { i: 0 };
154
+ gsap.to(frame, {
155
+ i: images.length - 1,
156
+ snap: "i",
157
+ ease: "none",
158
+ scrollTrigger: { trigger: section, start: "top top", end: "+=3000",
159
+ pin: true, scrub: 1 },
160
+ onUpdate: () => ctx.drawImage(images[frame.i], 0, 0),
161
+ });
162
+ ```
163
+
164
+ Do not start this until every frame is decoded, or the first scroll shows gaps.
165
+ On a slow connection, offer a static hero instead.
166
+
167
+ ## Failures
168
+
169
+ | Symptom | Cause |
170
+ |---|---|
171
+ | Pin does nothing | an ancestor has `overflow: hidden` |
172
+ | Motion lags behind scroll | easing inside a scrubbed timeline |
173
+ | Beats drift after an edit | separate triggers instead of one timeline |
174
+ | Wrong positions after load | images without dimensions — refresh |
175
+ | Unusable on mobile | desktop sequence scaled instead of replaced |
176
+ | Flicker when the pin engages | missing `anticipatePin` |
@@ -0,0 +1,143 @@
1
+ # Accessibility
2
+
3
+ Reduced motion is the part people get wrong, and they get it wrong in the same
4
+ way: they treat it as an off switch.
5
+
6
+ ## Reduced motion is a design
7
+
8
+ `prefers-reduced-motion: reduce` is set by people who get motion sickness,
9
+ vertigo, or migraines from movement — and by people who simply find it
10
+ distracting. Vestibular triggers are specific: **large travel, parallax,
11
+ zooming, spinning, and anything the visitor did not initiate.** A cross-fade is
12
+ usually fine. A modal scaling from 0.96 to 1 is usually fine.
13
+
14
+ So the question is never "animate or not". It is: *what was this motion saying,
15
+ and how do I say it without the travel?*
16
+
17
+ | Full | Reduced | Why |
18
+ |---|---|---|
19
+ | Section slides up 60px and fades | fades over 200ms | the arrival still reads |
20
+ | Logo draws itself over 1.2s | appears drawn, holds | the brand moment survives |
21
+ | Loader spins | pulses opacity gently | "still working" is the message |
22
+ | Parallax layers | static, correctly composed | parallax says nothing essential |
23
+ | Scroll-scrubbed sequence | each state on entry, no scrub | the content still arrives |
24
+ | Card lifts and scales on hover | border and shadow change | the affordance is what matters |
25
+ | Page transition wipe | instant swap | it was continuity decoration |
26
+
27
+ **A loader with nothing moving reads as a page that has hung** — which is the
28
+ one thing a loader exists to rule out. Keep a gentle opacity pulse.
29
+
30
+ ## Implementing it
31
+
32
+ `gsap.matchMedia` handles the preference *changing mid-session*, which a plain
33
+ check does not:
34
+
35
+ ```ts
36
+ const mm = gsap.matchMedia();
37
+
38
+ mm.add(
39
+ {
40
+ motion: "(prefers-reduced-motion: no-preference)",
41
+ reduced: "(prefers-reduced-motion: reduce)",
42
+ },
43
+ (context) => {
44
+ const { reduced } = context.conditions as Record<string, boolean>;
45
+
46
+ if (reduced) {
47
+ gsap.set("[data-item]", { autoAlpha: 1, y: 0 });
48
+ return;
49
+ }
50
+
51
+ gsap.from("[data-item]", { y: 60, autoAlpha: 0, stagger: 0.06 });
52
+ },
53
+ );
54
+
55
+ ```
56
+
57
+ Built inside a `useGSAP` body this needs no explicit teardown — `matchMedia`
58
+ registers with the active context and is reverted with it.
59
+
60
+ Everything built in the losing branch is reverted when the preference flips.
61
+ Without `matchMedia`, a visitor who turns reduced motion on mid-session keeps
62
+ the animation they asked to stop.
63
+
64
+ **The reduced branch must still set the end state.** A `from` tween that never
65
+ runs leaves the element at its authored start — invisible. This is the most
66
+ common reduced-motion bug, and it hides content rather than just skipping
67
+ motion.
68
+
69
+ ## Announcement
70
+
71
+ - A loader that blocks content: `role="status"` and an accessible name. It
72
+ announces politely without stealing focus.
73
+ - Decorative artwork: `aria-hidden="true"`, and make sure nothing inside it is
74
+ focusable.
75
+ - **Never both.** An `sr-only` label inside an `aria-hidden` container is never
76
+ read — it is dead markup.
77
+ - An overlay covering the page: the content beneath is still in the accessibility
78
+ tree. For a transient splash that is the right outcome — the visitor reaches
79
+ the content immediately. For a modal it is not, and you need `inert` or
80
+ `aria-modal` with focus management.
81
+
82
+ ## Focus
83
+
84
+ - Never destroy the focus ring. A transform that moves an element out from under
85
+ its outline, or an `overflow: hidden` ancestor clipping it mid-animation, both
86
+ count.
87
+ - Hover and focus should produce the same visual state. An effect that only
88
+ exists on `:hover` is invisible to a keyboard.
89
+ - An animation must not move a focused element out of the viewport.
90
+ - If an animation reveals interactive content, it must not be focusable before
91
+ it is visible. `autoAlpha` handles this; plain `opacity` does not.
92
+
93
+ ## Timing
94
+
95
+ - Nothing essential should depend on motion. If the animation is the only thing
96
+ that says "saved", that is a failure for anyone who cannot see it.
97
+ - Anything auto-playing for longer than 5 seconds and looping needs a way to
98
+ stop it — WCAG 2.2.2. A marquee of logos qualifies.
99
+ - Do not delay content the visitor came for behind an entrance. An intro that
100
+ cannot be skipped needs a hard time ceiling, and dismissal should be a race
101
+ against that ceiling rather than a sequence that can hang.
102
+
103
+ ## Flashing
104
+
105
+ Nothing should flash more than three times per second — WCAG 2.3.1. Rapid
106
+ opacity or colour cycling, scramble effects at high speed, and strobing hover
107
+ states all get close.
108
+
109
+ ## Touch
110
+
111
+ - Gate hover effects on `(hover: hover) and (pointer: fine)`. On touch,
112
+ `:hover` sticks after a tap and the state never clears.
113
+ - A cursor follower on a touch device is dead weight sitting in a corner.
114
+ - Anything that only responds to drag needs a keyboard path to the same outcome.
115
+
116
+ ## A marker that means "current"
117
+
118
+ A pill, underline or tint whose *position* says which tab, section or page is
119
+ current is a user-interface component, not decoration. WCAG 1.4.11 asks 3:1
120
+ against what it sits on. Motion makes a marker easy to notice while it travels;
121
+ at rest it has to be readable without that.
122
+
123
+ Brand fills rarely manage it on a light ground. `#FFD400` on white is 1.43:1,
124
+ and the soft brand tints are lower still. That does not rule a tint out — it
125
+ rules out the tint being the **only** thing that says "current". Carry the
126
+ state twice more:
127
+
128
+ - **In the label.** The current item keeps a colour that meets text contrast
129
+ even while the marker has moved off to follow the pointer.
130
+ - **In `aria-current`.** `page` for a route, `location` for a section inside
131
+ the page being read. This is the part that survives with no eyes on it.
132
+
133
+ ## Checklist
134
+
135
+ - Is there a reduced-motion branch, and does it still communicate?
136
+ - Does the reduced branch set the end state, not just skip the tween?
137
+ - Is `matchMedia` used, so a mid-session change is honoured?
138
+ - Is the artwork either announced or hidden — and not both?
139
+ - Does hover have a focus equivalent?
140
+ - Is the focus ring intact throughout?
141
+ - Can a long loop be stopped?
142
+ - Does anything essential exist only as motion?
143
+ - Does a "current" marker reach 3:1 — or is the state also in the label and `aria-current`?
@@ -0,0 +1,233 @@
1
+ # Core GSAP
2
+
3
+ The API you actually reach for. Plugin-specific surfaces live in their own
4
+ files; this is what is true everywhere.
5
+
6
+ ## Imports
7
+
8
+ Every plugin is in the public package. Import each from its own subpath so the
9
+ bundler can drop the ones a route does not use — never `gsap/all`.
10
+
11
+ ```ts
12
+ import gsap from "gsap";
13
+ import { ScrollTrigger } from "gsap/ScrollTrigger";
14
+ import { DrawSVGPlugin } from "gsap/DrawSVGPlugin";
15
+ ```
16
+
17
+ Register once, at module scope of the file that owns the animation:
18
+
19
+ ```ts
20
+ gsap.registerPlugin(useGSAP, ScrollTrigger, DrawSVGPlugin);
21
+ ```
22
+
23
+ Registering `useGSAP` alongside them is what makes GSAP warn about a missing
24
+ plugin instead of silently ignoring the property that needed it.
25
+
26
+ Available here, all free, all typed: `ScrollTrigger`, `ScrollSmoother`,
27
+ `ScrollToPlugin`, `Observer`, `Draggable`, `InertiaPlugin`, `Flip`, `SplitText`,
28
+ `ScrambleTextPlugin`, `TextPlugin`, `DrawSVGPlugin`, `MorphSVGPlugin`,
29
+ `MotionPathPlugin`, `MotionPathHelper`, `CustomEase`, `CustomWiggle`,
30
+ `CustomBounce`, `Physics2DPlugin`, `PhysicsPropsPlugin`, `GSDevTools`,
31
+ `PixiPlugin`, `EaselPlugin`, `CSSRulePlugin`, `EasePack`.
32
+
33
+ ## The four verbs
34
+
35
+ ```ts
36
+ gsap.to(target, vars); // from current state to vars
37
+ gsap.from(target, vars); // from vars to current state
38
+ gsap.fromTo(target, fromVars, to); // explicit both ends
39
+ gsap.set(target, vars); // instant, no tween
40
+ ```
41
+
42
+ `from` is convenient and it is also the usual cause of a flash: the element
43
+ paints at its natural state before the tween applies. `fromTo` with an explicit
44
+ start, or a `gsap.set` before, is what removes that. In React, `useGSAP` runs in
45
+ a layout effect so the set lands before paint — that is most of why it matters.
46
+
47
+ ## Targets
48
+
49
+ A selector string, an element, a ref's `.current`, an array, or an object.
50
+ Inside a `useGSAP` or `gsap.context` scope, selector strings resolve against the
51
+ scope rather than the document — which is the whole reason to use one.
52
+
53
+ ```ts
54
+ gsap.to("[data-card]", { ... }); // scoped, when inside useGSAP
55
+ gsap.to(ref.current, { ... }); // explicit
56
+ gsap.to({ v: 0 }, { v: 100, onUpdate }); // tween a plain object
57
+ ```
58
+
59
+ That last form is how you animate anything GSAP cannot touch directly — a
60
+ canvas value, a counter, a Three.js material, a CSS variable on a distant node.
61
+
62
+ ## Properties
63
+
64
+ Transforms are first-class and composable, and GSAP writes them as one matrix
65
+ rather than fighting over `transform`:
66
+
67
+ ```ts
68
+ { x: 100, y: -20, xPercent: -50, rotation: 45, scale: 1.2, skewX: 8,
69
+ transformOrigin: "50% 100%" }
70
+ ```
71
+
72
+ `xPercent`/`yPercent` are percentages of the element's own size, which is what
73
+ centres something without knowing its dimensions.
74
+
75
+ `autoAlpha` is `opacity` plus `visibility` — it hides the element from pointer
76
+ events at 0, which plain `opacity` does not.
77
+
78
+ CSS variables work, and are the cheapest way to drive something whose styling
79
+ lives in CSS:
80
+
81
+ ```ts
82
+ gsap.to(root, { "--glow-strength": 1, duration: 0.6 });
83
+ ```
84
+
85
+ ## Useful vars
86
+
87
+ ```ts
88
+ {
89
+ duration, delay, ease,
90
+ repeat: -1, // forever
91
+ yoyo: true, // reverse on alternate repeats
92
+ repeatDelay: 0.4,
93
+ stagger: 0.06, // or { each, amount, from, ease }
94
+ overwrite: "auto", // kill conflicting tweens on the same props
95
+ paused: true,
96
+ onComplete, onUpdate, onStart,
97
+ }
98
+ ```
99
+
100
+ `overwrite: "auto"` is the fix for the classic hover bug where leaving and
101
+ re-entering fast leaves an element halfway. Use it on anything an event can
102
+ retrigger.
103
+
104
+ ## `gsap.utils`
105
+
106
+ Small functions that remove most of the arithmetic from animation code.
107
+
108
+ ```ts
109
+ gsap.utils.clamp(0, 1, v);
110
+ gsap.utils.mapRange(0, 500, 0, 1, scrollY); // remap one range onto another
111
+ gsap.utils.interpolate("#000", "#fff", 0.5);
112
+ gsap.utils.wrap(0, 10, 13); // → 3, for infinite loops
113
+ gsap.utils.snap(0.1, 0.34); // → 0.3
114
+ gsap.utils.random(-20, 20, 1);
115
+ gsap.utils.toArray("[data-card]"); // NodeList → real array
116
+ gsap.utils.selector(ref); // scoped selector function
117
+ gsap.utils.shuffle(items);
118
+ ```
119
+
120
+ `mapRange` and `wrap` are the two that most often replace ten lines of
121
+ hand-rolled maths in a scroll or marquee.
122
+
123
+ ## `quickTo` and `quickSetter`
124
+
125
+ For values driven by a high-frequency event — pointer move, scroll, rAF —
126
+ creating a tween per event allocates garbage and fights itself.
127
+
128
+ ```ts
129
+ const moveX = gsap.quickTo(el, "x", { duration: 0.5, ease: "power3" });
130
+ const moveY = gsap.quickTo(el, "y", { duration: 0.5, ease: "power3" });
131
+
132
+ // In the handler — reuses one tween, no allocation:
133
+ moveX(e.clientX);
134
+ moveY(e.clientY);
135
+ ```
136
+
137
+ `quickSetter` is the same idea with no interpolation, for when you are already
138
+ smoothing the value yourself:
139
+
140
+ ```ts
141
+ const setX = gsap.quickSetter(el, "x", "px");
142
+ ```
143
+
144
+ This is the single most important performance tool for pointer work. See
145
+ [interaction.md](interaction.md).
146
+
147
+ ## `matchMedia`
148
+
149
+ Different strategies per breakpoint or preference, with automatic teardown when
150
+ the condition stops matching.
151
+
152
+ ```ts
153
+ const mm = gsap.matchMedia();
154
+
155
+ mm.add(
156
+ {
157
+ desktop: "(min-width: 768px)",
158
+ mobile: "(max-width: 767px)",
159
+ reduced: "(prefers-reduced-motion: reduce)",
160
+ },
161
+ (context) => {
162
+ const { desktop, reduced } = context.conditions as Record<string, boolean>;
163
+ if (reduced) { /* the reduced design */ return; }
164
+ gsap.to(el, { x: desktop ? 200 : 60 });
165
+ },
166
+ );
167
+
168
+ // Teardown, when it was not created inside a context:
169
+ mm.revert();
170
+ ```
171
+
172
+ Everything created inside the callback is reverted when the condition stops
173
+ matching. A plain `window.matchMedia` check leaves the first animation running
174
+ forever when a visitor turns reduced motion on mid-session, or rotates a tablet.
175
+
176
+ **Created inside a `useGSAP` or `gsap.context` body, it reverts itself with
177
+ that context** — `MatchMedia`'s constructor registers with whatever context is
178
+ active (verified against gsap 3.15), and `Context.add` makes itself active while your
179
+ function runs. An explicit `mm.revert()` there is redundant, not wrong. Created
180
+ outside one — module scope, a plain `useEffect` — it is yours to revert. See
181
+ [react-nextjs.md](react-nextjs.md).
182
+
183
+ ## `context`
184
+
185
+ Scopes selectors and collects everything for teardown. In React you get this
186
+ through `useGSAP` and rarely call it directly.
187
+
188
+ ```ts
189
+ const ctx = gsap.context(() => {
190
+ gsap.to(".card", { y: 0 }); // resolved inside `root`
191
+ }, root);
192
+
193
+ ctx.revert(); // undoes everything, restores inline styles
194
+ ```
195
+
196
+ `revert()` restores original inline styles; `kill()` only stops the tweens. For
197
+ a component unmount you almost always want `revert`.
198
+
199
+ **A scope does not reach inside plugin config.** `motionPath.path` and
200
+ `morphSVG.shape` are resolved by the plugin against the whole document, so a
201
+ fixed id in a component rendered twice makes both instances drive the first
202
+ one's geometry — with no error. Namespace those ids per instance.
203
+
204
+ ## Control
205
+
206
+ ```ts
207
+ tl.play(); tl.pause(); tl.reverse(); tl.restart();
208
+ tl.progress(0.5); // jump
209
+ tl.timeScale(2); // twice as fast, live
210
+ tl.seek("label");
211
+ ```
212
+
213
+ Tweening a timeline's own progress is how you smooth a jumpy external value:
214
+
215
+ ```ts
216
+ gsap.to(tl, { progress: uploaded, duration: 0.4, overwrite: true });
217
+ ```
218
+
219
+ ## Easing
220
+
221
+ Named: `power1`–`power4`, `back`, `elastic`, `bounce`, `circ`, `expo`, `sine`,
222
+ `steps(n)`, `none`, each with `.in`/`.out`/`.inOut`. Which to pick is a craft
223
+ question — see [motion-design.md](motion-design.md).
224
+
225
+ `CustomEase` takes an SVG path and is free:
226
+
227
+ ```ts
228
+ CustomEase.create("brand", "M0,0 C0.16,1 0.3,1 1,1");
229
+ gsap.to(el, { y: 0, ease: "brand" });
230
+ ```
231
+
232
+ `CustomWiggle` and `CustomBounce` register named eases the same way, for shake
233
+ and impact without hand-built keyframes.