@mehshekari/gsap-motion 3.1.1 → 3.3.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.
package/README.md CHANGED
@@ -28,7 +28,7 @@ The audit needs no install. Pin the version, so a new rule cannot fail your
28
28
  build without warning:
29
29
 
30
30
  ```json
31
- "lint": "eslint && npx @mehshekari/gsap-motion@3.1.1 audit src --quiet"
31
+ "lint": "eslint && npx @mehshekari/gsap-motion@3.3.0 audit src --quiet"
32
32
  ```
33
33
 
34
34
  Or install it with `npm install --save-dev --save-exact @mehshekari/gsap-motion`.
@@ -257,6 +257,23 @@ function doctor() {
257
257
  );
258
258
  }
259
259
 
260
+ /**
261
+ * Which adapters the skill loads here, read the way SKILL.md's setup step 3
262
+ * describes. Deterministic, so the stack is looked up rather than guessed.
263
+ */
264
+ const picked = new Set();
265
+ if (deps.next) picked.add("react").add("next");
266
+ if (deps["@react-three/fiber"]) picked.add("react").add("r3f");
267
+ else if (deps.three) picked.add("three");
268
+ if (deps.react) picked.add("react");
269
+ if (deps.vue || deps.nuxt) picked.add("vue");
270
+ if (deps.svelte || deps["@sveltejs/kit"]) picked.add("svelte");
271
+ if (deps.astro) picked.add("astro");
272
+ if (picked.size === 0) picked.add("vanilla");
273
+
274
+ const ORDER = ["react", "next", "r3f", "three", "vue", "svelte", "astro", "vanilla"];
275
+ report("ok", `Adapters: ${ORDER.filter((name) => picked.has(name)).join(", ")}`);
276
+
260
277
  const rules = existsSync(join(process.cwd(), "ANIMATION.md"));
261
278
  report(
262
279
  rules ? "ok" : "info",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mehshekari/gsap-motion",
3
- "version": "3.1.1",
3
+ "version": "3.3.0",
4
4
  "description": "Install the gsap-creative-animation Agent Skill, and audit GSAP animation for leaks, jank and accessibility failures — with npx.",
5
5
  "keywords": [
6
6
  "gsap",
package/skill/SKILL.md CHANGED
@@ -4,7 +4,7 @@ description: Senior motion engineer for GSAP. Turns a visual or interaction idea
4
4
  license: MIT. See LICENSE; GSAP itself is licensed separately, see NOTICE.md
5
5
  compatibility: Any Agent Skills client; written for Claude Code. GSAP 3.13+, where every plugin is free. React guidance targets @gsap/react 2.x, React 18/19 and the Next.js App Router. The audit scripts need Node.js 18+ and nothing else.
6
6
  metadata:
7
- version: "3.1.1"
7
+ version: "3.3.0"
8
8
  verified-gsap: "3.15"
9
9
  verified-gsap-react: "2.1"
10
10
  ---
@@ -18,8 +18,8 @@ Core principles:
18
18
 
19
19
  - **Motion carries meaning or it goes.** Every animation answers "what does
20
20
  this tell the visitor?" Hierarchy, causality, continuity, state, or
21
- personality. An animation that answers none of those is decoration with a
22
- frame cost, and the right move is to delete it.
21
+ personality. One that answers none of those is decoration with a frame cost:
22
+ delete it.
23
23
  - **A timeline, not a pile of tweens.** Five independent tweens with hand-tuned
24
24
  delays is a choreography nobody can change. One timeline with labels and
25
25
  relative positions is the same animation you can actually retime.
@@ -33,23 +33,27 @@ Core principles:
33
33
 
34
34
  ## Setup
35
35
 
36
- 1. **Everything is free.** GSAP became 100% free, every former Club plugin
37
- included, with 3.13 (April 2025). Check `package.json` for what the project
38
- has — `gsap`, and `@gsap/react` for React — before writing imports. Install
39
- steps for a membership, a token, an `.npmrc` or `gsap-bonus.tgz` come from
40
- stale training data: stop.
36
+ 1. **Everything is free.** GSAP became 100% free with 3.13 (April 2025), every
37
+ former Club plugin included. Check `package.json` for what the project has —
38
+ `gsap`, and `@gsap/react` for React — before writing imports. Install steps
39
+ for a membership, a token, an `.npmrc` or `gsap-bonus.tgz` are stale
40
+ training data: stop.
41
41
  2. **Read the project's own animation rules first. They outrank this skill.**
42
- Look for `ANIMATION.md` at the repository root, then for a file that
43
- `AGENTS.md`, `CLAUDE.md` or similar points to. Open
42
+ Look for `ANIMATION.md` at the root, then a file `AGENTS.md`, `CLAUDE.md` or
43
+ similar points to. Open
44
44
  [reference/project-rules.md](reference/project-rules.md) only when there is
45
- no such file, or for its RTL, colour and waiver rules.
45
+ none, or for its RTL, colour and waiver rules.
46
46
  3. **Load by the size of the request, and nothing "for completeness".**
47
47
  - **One element, one tween or interaction** — a hover, a fade, a retime:
48
48
  [motion-design.md](reference/motion-design.md) and the command's own
49
49
  reference. Nothing else.
50
- - **A component or section:** also
51
- [react-nextjs.md](reference/react-nextjs.md) if `package.json` has
52
- `react`, or [frameworks.md](reference/frameworks.md) if it does not.
50
+ - **A component or section:** also this project's adapter, from
51
+ `package.json`: `next` → [react](adapter/react.md) +
52
+ [next](adapter/next.md); `@react-three/fiber` → react +
53
+ [r3f](adapter/r3f.md); `three` → [three](adapter/three.md); `nuxt` →
54
+ [vue](adapter/vue.md); `svelte` → [svelte](adapter/svelte.md); `astro` →
55
+ [astro](adapter/astro.md) plus any island's own; nothing detected →
56
+ [vanilla](adapter/vanilla.md).
53
57
  - **A sequence, scroll scene, intro, transition or audit:** also
54
58
  [core-gsap.md](reference/core-gsap.md), and what
55
59
  [routing.md](reference/routing.md) adds for that request.
@@ -68,7 +72,7 @@ Core principles:
68
72
  | `transition [target]` | Build | Page, route, modal and layout transitions | [reference/flip.md](reference/flip.md) |
69
73
  | `loader` | Build | Loading and progress animation | [preset/loader.md](preset/loader.md) |
70
74
  | `intro` | Build | Site or section entry sequence | [preset/cinematic.md](preset/cinematic.md) |
71
- | `three [target]` | Build | GSAP orchestrating Three.js / R3F | [reference/three-r3f.md](reference/three-r3f.md) |
75
+ | `three [target]` | Build | GSAP orchestrating Three.js / R3F | [adapter/three.md](adapter/three.md) |
72
76
  | `audit [target]` | Evaluate | Check an existing animation: leaks, cost, a11y | [reference/performance.md](reference/performance.md) |
73
77
  | `tune [target]` | Refine | Retime, re-ease, fix rhythm — no new motion | [reference/motion-design.md](reference/motion-design.md) |
74
78
  | `strip [target]` | Fix | Remove motion that is not earning its place | [reference/motion-design.md](reference/motion-design.md) |
@@ -76,12 +80,11 @@ Core principles:
76
80
  Routing:
77
81
 
78
82
  - **No argument, or no target:** read [routing.md](reference/routing.md) and
79
- ask what should move. A whole page or site with nothing in it named — "add
80
- some animation to my homepage" — has no target. Never invent one.
81
- - **An explicit or clearly implied command:** load its reference and follow it.
82
- - **A description rather than a command** ("make this feel like Apple", "the
83
- hero is boring"): that is `animate`. Classify the intent with routing.md's
84
- table, then load what that classification needs.
83
+ ask what should move. "Add some animation to my homepage" names no target:
84
+ never invent one.
85
+ - **A command, explicit or implied:** load its reference and follow it.
86
+ - **A description** ("make this feel like Apple", "the hero is boring"): that is
87
+ `animate`. Classify with routing.md's table, then load what that needs.
85
88
  - **Two commands fit:** ask once. A scroll-driven text reveal is `scroll` and
86
89
  `text` together, not a coin toss.
87
90
 
@@ -109,7 +112,8 @@ intent → precedent → targets → trigger → motion language → technique
109
112
  8. **Implementation** — typed, cleaned up, responsive.
110
113
  9. **Performance** — transform and opacity; measure before defending anything else.
111
114
  10. **Accessibility** — the reduced-motion design, focus, keyboard.
112
- 11. **Validate** — the checklist at the end of [motion-design.md](reference/motion-design.md).
115
+ 11. **Validate** — [refine.md](reference/refine.md): audit, fix in order,
116
+ re-run, then the checklist in [motion-design.md](reference/motion-design.md).
113
117
 
114
118
  ## Output format
115
119
 
@@ -122,26 +126,29 @@ For anything beyond a one-line tweak, answer in this shape, each part short:
122
126
  - **Implementation** — complete code. Not pseudocode, not an outline.
123
127
  - **Usage** — how to mount it.
124
128
  - **Notes** — responsive behaviour, reduced motion, cost, dev-only tooling.
129
+ - **Verification** — one line per check with its evidence, and what you did not
130
+ check. Silence reads as "checked and fine".
125
131
 
126
- When a simpler technique does what the user asked for, **write only the
127
- simpler one**. Say in Analysis which technique you declined and why, and offer
128
- the requested version in one sentence; write it only if the user still wants it
129
- after that. Never quietly substitute, and never ship both.
132
+ When a simpler technique does what was asked, **write only that one**. Say in
133
+ Analysis what you declined and why, and offer the requested version in a
134
+ sentence — write it only if they still want it. Never substitute quietly, and
135
+ never ship both.
130
136
 
131
137
  ## Reference map
132
138
 
133
139
  - **Craft and core** — [motion-design](reference/motion-design.md) ·
134
- [core-gsap](reference/core-gsap.md) · [timeline](reference/timeline.md)
140
+ [core-gsap](reference/core-gsap.md) · [timeline](reference/timeline.md) ·
141
+ [devices](reference/devices.md) · [refine](reference/refine.md)
135
142
  - **Domains** — [scrolltrigger](reference/scrolltrigger.md) ·
136
143
  [svg](reference/svg.md) · [text](reference/text.md) ·
137
- [interaction](reference/interaction.md) · [flip](reference/flip.md) ·
138
- [three-r3f](reference/three-r3f.md)
139
- - **Constraints** — [react-nextjs](reference/react-nextjs.md) ·
140
- [frameworks](reference/frameworks.md) ·
141
- [performance](reference/performance.md) ·
144
+ [interaction](reference/interaction.md) · [flip](reference/flip.md)
145
+ - **Adapters**, one per stack, chosen in setup step 3 — the same questions for
146
+ each: lifecycle, scope, server rendering, per-frame values, routes, failures,
147
+ audit.
148
+ - **Constraints** — [performance](reference/performance.md) ·
142
149
  [accessibility](reference/accessibility.md) ·
143
150
  [project-rules](reference/project-rules.md)
144
- - **Presets**, starting points to adapt — [reveal](preset/reveal.md) ·
151
+ - **Presets** to adapt — [reveal](preset/reveal.md) ·
145
152
  [magnetic](preset/magnetic.md) · [cursor](preset/cursor.md) ·
146
153
  [marquee](preset/marquee.md) · [card-stack](preset/card-stack.md) ·
147
154
  [loader](preset/loader.md) · [cinematic](preset/cinematic.md) ·
@@ -156,13 +163,12 @@ after that. Never quietly substitute, and never ship both.
156
163
 
157
164
  ## Tools
158
165
 
159
- Both scripts catch failures that are **silent**. Run them from the project
160
- root; `<skill-dir>` is where this skill is installed.
166
+ Both scripts catch **silent** failures. Run them from the project root;
167
+ `<skill-dir>` is where this skill is installed.
161
168
 
162
169
  ```bash
163
- # Leaks, per-event tweens, layout properties, eased loops, an onComplete that
164
- # never fires, shared plugin ids, unregistered plugins, dev tooling left in.
165
- # Exit 1 on an error.
170
+ # Leaks, per-event and per-frame tweens, layout and paint properties, loops,
171
+ # shared plugin ids, unregistered plugins, dev tooling. Exit 1 on an error.
166
172
  node <skill-dir>/scripts/audit-gsap.mjs [path...] # default: src
167
173
  node <skill-dir>/scripts/audit-gsap.mjs src --quiet # errors only
168
174
  node <skill-dir>/scripts/audit-gsap.mjs src --json # machine-readable
@@ -173,10 +179,11 @@ node <skill-dir>/scripts/audit-svg.mjs --morph <a.svg> <b.svg>
173
179
  node <skill-dir>/scripts/audit-svg.mjs --hues <file.svg> # also flag hue literals
174
180
  ```
175
181
 
176
- Run `audit-gsap` first in an `audit` and after writing any animation. A false
177
- finding is a bug in the script, not something to work around.
182
+ Run `audit-gsap` first in an `audit` and after writing any animation — as a
183
+ Claude Code plugin, a hook already runs it on every edit and hands back what it
184
+ finds. A false finding is a bug in the script, not something to work around.
178
185
 
179
186
  `MotionPathHelper`, `GSDevTools`, `markers: true` and
180
- `MorphSVGPlugin.findShapeIndex()` are development-only. A static import ships
181
- whatever `if` surrounds it: import them dynamically behind a `NODE_ENV` check,
182
- and delete the block once their value is baked in.
187
+ `MorphSVGPlugin.findShapeIndex()` are development-only, and a static import
188
+ ships whatever `if` surrounds it. Import them dynamically behind a `NODE_ENV`
189
+ check, and delete the block once their value is baked in.
@@ -0,0 +1,111 @@
1
+ # Astro
2
+
3
+ Astro ships no JavaScript unless you ask for it, and its pages can be swapped
4
+ without a reload. Both facts decide where an animation lives.
5
+
6
+ ## Where it is created
7
+
8
+ In a component's `<script>`, which Astro bundles and defers. With view
9
+ transitions enabled, a page swap does not re-run it, so build on the page-load
10
+ event rather than at the top level:
11
+
12
+ ```astro
13
+ <script>
14
+ import gsap from "gsap";
15
+
16
+ let ctx;
17
+
18
+ document.addEventListener("astro:page-load", () => {
19
+ ctx = gsap.context(() => {
20
+ gsap.from("[data-item]", { autoAlpha: 0, y: 24, stagger: 0.06 });
21
+ });
22
+ });
23
+
24
+ document.addEventListener("astro:before-swap", () => ctx?.revert());
25
+ </script>
26
+ ```
27
+
28
+ `astro:page-load` fires on the first load and after every swap, so this one
29
+ shape covers both. Without view transitions the event still fires once, so the
30
+ same code is correct either way.
31
+
32
+ Inside a React, Vue or Svelte island, use that framework's adapter instead —
33
+ the island owns its own lifecycle.
34
+
35
+ ## Where it is torn down
36
+
37
+ `astro:before-swap`, with `ctx.revert()`. Skipping it leaves the previous page's
38
+ ScrollTriggers and listeners bound to elements that are about to be replaced,
39
+ and they accumulate for as long as the visit lasts.
40
+
41
+ Timers, listeners and observers are yours, in the same handler.
42
+
43
+ ## Scope
44
+
45
+ Astro's `<script>` is page-wide, not component-scoped: a component used twice on
46
+ a page runs its script once, and selector strings match both instances. Either
47
+ animate them as a set — which is usually what you want — or pass the root in
48
+ with a `data-*` attribute and build one context per matching element.
49
+
50
+ Plugin config is not scoped either, so generate ids per instance rather than
51
+ hardcoding them.
52
+
53
+ ## Server rendering and hydration
54
+
55
+ Astro components render to HTML on the server and ship no client JavaScript of
56
+ their own, so there is no hydration to mismatch. The script runs only in the
57
+ browser: reading `window` in it is safe, and the markup it animates is already
58
+ painted.
59
+
60
+ `client:*` directives decide when an island hydrates. An animation inside one
61
+ does not run until it does — `client:visible` on a section that animates on
62
+ entrance is a race worth avoiding; prefer `client:load` there, or keep the
63
+ animation in the page's own script.
64
+
65
+ ## Reaching the element
66
+
67
+ `data-*` attributes in the markup, queried inside the context. Astro scopes its
68
+ component styles by adding its own attributes to elements, so a class in the
69
+ markup is not always the class in the DOM — a `data-*` attribute you wrote is.
70
+
71
+ ## Every frame
72
+
73
+ `gsap.quickTo` or `quickSetter`, created once inside the context. There is no
74
+ component state here to get wrong, which removes the most common React mistake
75
+ and leaves the ordinary one: allocating a tween per event.
76
+
77
+ ## Page and route changes
78
+
79
+ The events are the contract:
80
+
81
+ - `astro:page-load` — build, on first load and after every swap
82
+ - `astro:before-swap` — revert, before the old page goes
83
+ - `astro:after-swap` — for anything that must run against the new DOM before it
84
+ paints, such as `ScrollTrigger.refresh()`
85
+
86
+ View transitions are opt-in: they come from Astro's router component in the
87
+ layout, named `<ClientRouter />` in current versions and `<ViewTransitions />`
88
+ in older ones. Check which one the project imports before telling anyone to add
89
+ it.
90
+
91
+ ## Failures
92
+
93
+ | Symptom | Cause |
94
+ |---|---|
95
+ | Animation runs on the first page only | built at the top of `<script>` instead of on `astro:page-load` |
96
+ | Triggers pile up while browsing | no `astro:before-swap` revert |
97
+ | The script never runs | `is:inline`, which skips bundling, so the bare import fails |
98
+ | Both instances animate as one | a page-wide script with a document-wide selector |
99
+ | An entrance animation starts late | the island hydrates on `client:visible` |
100
+
101
+ ## What the audit covers
102
+
103
+ From 3.2 the audit reads the `<script>` block of `.astro` files, with offsets
104
+ kept, so reported lines match the file. It checks that a context built on
105
+ `astro:page-load` is reverted on `astro:before-swap`, along with every rule that
106
+ is not React-specific.
107
+
108
+ ## Versions
109
+
110
+ Verified against `gsap@3.15`. Written for `astro@7`; the swap events are the
111
+ same in Astro 5 and later, though the router component was renamed.
@@ -0,0 +1,99 @@
1
+ # Next.js
2
+
3
+ The App Router, with Server Components. Read [react.md](react.md) first: this is
4
+ only what Next adds.
5
+
6
+ ## Where it is created
7
+
8
+ In a `"use client"` component, in the `useGSAP` body react.md describes. Keep
9
+ that component a **leaf**, so the page above it stays a Server Component.
10
+
11
+ **Converting a Server Component is part of the cost, and it is priced before the
12
+ design.** *Is GSAP already on this route?* If a sibling section loads `gsap` and
13
+ `ScrollTrigger`, the marginal cost is this component's own JavaScript — a
14
+ kilobyte or two rather than thirty-odd, which usually settles the argument. If
15
+ nothing on the route loads GSAP, this animation buys the whole library for one
16
+ reveal. *Does anything claim otherwise?* A comment saying the section ships no
17
+ client JavaScript stops being true the moment you add the hook; update it in the
18
+ same change.
19
+
20
+ Say so in the Notes of your answer: "it becomes a client component" is a fact
21
+ the reader needs.
22
+
23
+ ## Where it is torn down
24
+
25
+ By the context, as in react.md. A navigation unmounts the page, so anything that
26
+ must survive one belongs in the layout.
27
+
28
+ ## Scope
29
+
30
+ As in react.md, and `useId` matters more here: the markup is rendered on the
31
+ server and again on the client, and a counter would disagree between them.
32
+
33
+ ## Server rendering and hydration
34
+
35
+ - **`"use client"` module scope still runs during server rendering.**
36
+ Registering plugins there is fine — GSAP defers anything touching `window` —
37
+ but reading `window` or `document` at module scope is not.
38
+ - **Data and copy come down as props.** A client component importing a content
39
+ or locale module bundles every locale into the browser payload. Large
40
+ generated data — path tables, keyframe sets — is handed down too.
41
+ - Storage and media queries are read after mount, never during render.
42
+
43
+ ## Reaching the element
44
+
45
+ `next/image` renders the `<img>` itself and controls its `src`, `srcset` and
46
+ sizing:
47
+
48
+ - **Animate a wrapper you own**, not the image, when the effect needs a mask, a
49
+ clip or a transform that must survive the placeholder-to-loaded swap. Under
50
+ `fill` the image is positioned against the nearest positioned ancestor, so
51
+ that wrapper is already there.
52
+ - **A reveal that depends on the image waits for load**, with a short budget
53
+ after which it plays anyway. A cached image can finish before the hook runs,
54
+ and a mask opening over an empty box reads as a bug.
55
+ - If the animation starts by hiding a hero image, **measure what it does to
56
+ LCP** first. A mask over an image that is already painted usually avoids the
57
+ question.
58
+
59
+ ## Every frame
60
+
61
+ Refs and `quickTo`, never React state — and it costs more here, since state held
62
+ in a layout re-renders every page under it.
63
+
64
+ ## Page and route changes
65
+
66
+ The App Router unmounts the old tree before the new one paints, so an exit
67
+ animation has nowhere to play. Options, by cost:
68
+
69
+ 1. **Animate entrance only**, keyed off `usePathname()`. Honest, cheap, and what
70
+ most sites want.
71
+ 2. **An overlay that outlives the route**, mounted in the layout so the
72
+ navigation does not unmount it. See
73
+ [page-transition](../preset/page-transition.md).
74
+ 3. **`router.push` behind a played-out timeline** — intercept, play the exit,
75
+ navigate on complete. Breaks the back button's feel; use sparingly.
76
+
77
+ Then `ScrollTrigger.refresh()` once the new layout settles.
78
+
79
+ ## Failures
80
+
81
+ | Symptom | Cause |
82
+ |---|---|
83
+ | `window is not defined` during a build | `window` read at module scope in a client component |
84
+ | Hydration mismatch | storage, a media query or a random value read during render |
85
+ | The whole page ships as client JavaScript | `"use client"` on the page or layout instead of a leaf |
86
+ | A reveal opens over an empty box | the tween did not wait for the image to load |
87
+ | Positions are wrong after a navigation | no `ScrollTrigger.refresh()` once it settled |
88
+ | The exit animation never plays | written in the page the navigation unmounts |
89
+
90
+ ## What the audit covers
91
+
92
+ The React rules from react.md, on the same files. It reads `.ts`, `.tsx`, `.js`,
93
+ `.jsx` and `.mjs`, so `"use client"` placement, `next.config` and route
94
+ structure are yours to review.
95
+
96
+ ## Versions
97
+
98
+ Verified against `gsap@3.15` and `@gsap/react@2.1`. Written for `next@15` and
99
+ `next@16`, App Router.
@@ -0,0 +1,128 @@
1
+ # React Three Fiber
2
+
3
+ R3F is React's renderer for Three, so both lifecycles apply: read
4
+ [react.md](react.md) for the context and cleanup rules, and
5
+ [three.md](three.md) for what GSAP may tween on a Three object. This file is the
6
+ part that is neither.
7
+
8
+ ## Where it is created
9
+
10
+ In a `useGSAP` body inside the `<Canvas>` subtree, against refs to the objects:
11
+
12
+ ```tsx
13
+ "use client";
14
+
15
+ import { useGSAP } from "@gsap/react";
16
+ import gsap from "gsap";
17
+ import { useRef } from "react";
18
+ import type { Mesh } from "three";
19
+
20
+ export function Box() {
21
+ const mesh = useRef<Mesh>(null);
22
+
23
+ useGSAP(() => {
24
+ if (!mesh.current) return;
25
+ gsap.to(mesh.current.rotation, {
26
+ y: Math.PI * 2,
27
+ duration: 6,
28
+ ease: "none",
29
+ repeat: -1,
30
+ });
31
+ });
32
+
33
+ return (
34
+ <mesh ref={mesh}>
35
+ <boxGeometry />
36
+ <meshStandardMaterial />
37
+ </mesh>
38
+ );
39
+ }
40
+ ```
41
+
42
+ A camera move is where GSAP earns its place — easing a camera by hand is how
43
+ scroll-driven 3D ends up feeling mechanical:
44
+
45
+ ```ts
46
+ const { camera, invalidate } = useThree();
47
+
48
+ gsap.timeline({ defaults: { ease: "power2.inOut", duration: 2 } })
49
+ .to(camera.position, { x: 0, y: 1.5, z: 4 })
50
+ .to(camera.rotation, { x: -0.2 }, "<");
51
+ ```
52
+
53
+ ## Where it is torn down
54
+
55
+ The `useGSAP` context reverts the tweens, as in react.md.
56
+
57
+ R3F disposes what it created for elements it rendered, when they unmount.
58
+ Anything you built yourself — a geometry, material or texture made outside JSX,
59
+ a loader's result you cached — is yours to dispose.
60
+
61
+ ## Scope
62
+
63
+ There are no selector strings here: targets are refs. `{ scope: root }` still
64
+ matters for any DOM overlay the component renders beside the canvas.
65
+
66
+ ## Server rendering and hydration
67
+
68
+ The canvas is client-only. Import the component dynamically so Three is not in
69
+ the initial bundle, and render a fallback while it loads — that fallback is also
70
+ the reduced-motion and low-power design, so it is not wasted work.
71
+
72
+ ## Reaching the element
73
+
74
+ Refs, held where the object is declared. `useThree` reaches the camera, the
75
+ renderer and the scene without prop-drilling. Traversing by name binds to the
76
+ exporter's names, which the next export changes.
77
+
78
+ ## Every frame
79
+
80
+ **Never drive a 3D transform through React state.** `useState` per frame
81
+ re-renders the React tree for a value Three reads directly. Refs and GSAP only.
82
+
83
+ `useFrame` and GSAP are complementary, not competing: `useFrame` for continuous
84
+ logic that follows the clock, GSAP for transitions between states. Inside
85
+ `useFrame`, the rules are the ones for any per-frame scope — no allocation, no
86
+ state, no `setState` from a callback.
87
+
88
+ With `frameloop="demand"`, the canvas renders only when something asks it to, so
89
+ a GSAP tween that changes a Three value must ask:
90
+
91
+ ```ts
92
+ gsap.to(mesh.current.position, { y: 1, onUpdate: invalidate });
93
+ ```
94
+
95
+ Without that, the tween runs and nothing is drawn — the most confusing failure
96
+ in this file, because every value is correct.
97
+
98
+ ## Page and route changes
99
+
100
+ The canvas unmounts with the route, and R3F disposes its own objects. What
101
+ survives is anything you created outside JSX, plus any ScrollTrigger built
102
+ against a DOM element that is now gone — which the context reverts, provided it
103
+ was created inside the body.
104
+
105
+ ## Failures
106
+
107
+ | Symptom | Cause |
108
+ |---|---|
109
+ | Values change but nothing is drawn | `frameloop="demand"` with no `invalidate` from `onUpdate` |
110
+ | The whole tree re-renders while animating | 3D values held in React state |
111
+ | Nothing moves | tweened the object instead of `object.position` / `.rotation` |
112
+ | The tween starts before the object exists | no early return on a null ref |
113
+ | Memory grows across route changes | objects created outside JSX, never disposed |
114
+ | Scroll-driven camera drifts out of sync | a scrub with an ease on a child, instead of `ease: "none"` |
115
+
116
+ ## What the audit covers
117
+
118
+ The React rules from react.md, since these are `.tsx` files. From 3.2,
119
+ `useFrame` counts as a per-frame scope, so React state set inside it is reported
120
+ as `state-per-event`.
121
+
122
+ Disposal, GPU cost and the render loop are not checkable — read for them, and
123
+ measure with [performance.md](../reference/performance.md).
124
+
125
+ ## Versions
126
+
127
+ Verified against `gsap@3.15` and `@gsap/react@2.1`. Written for
128
+ `@react-three/fiber@8` and `@react-three/fiber@9`.