@mehshekari/gsap-motion 4.3.0 → 4.5.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
@@ -32,7 +32,7 @@ The audit needs no install. Pin the version, so a new rule cannot fail your
32
32
  build without warning:
33
33
 
34
34
  ```json
35
- "lint": "eslint && npx @mehshekari/gsap-motion@4.3.0 audit src --quiet"
35
+ "lint": "eslint && npx @mehshekari/gsap-motion@4.5.0 audit src --quiet"
36
36
  ```
37
37
 
38
38
  Or install it with `npm install --save-dev --save-exact @mehshekari/gsap-motion`.
@@ -74,15 +74,21 @@ Commands
74
74
  --morph <a> <b> Compare a morph pair
75
75
  --hues Also report hard-coded hues
76
76
  inspect <url> Watch a page animate and report what it did
77
- --at 0,300,900 When to capture, in ms after the page settles
77
+ --at 0,300,900 When to capture, in ms after --wait
78
+ --wait 400 A fixed wait after navigation; it does not detect a loader
78
79
  --scroll <px|sel> Scroll before capturing
79
80
  --hover <selector> Move the pointer onto an element
80
81
  --click <selector> Click an element
81
82
  --reduced --dark Emulate a preference
82
83
  --mobile 390x844 with touch input
84
+ --viewport 1440x900 The window's size; every report prints the one used
83
85
  --cpu 4 Throttle the CPU by this factor
86
+ --headed A visible Chrome on the GPU, for WebGL pages
87
+ --seek <name> Call window.<name>(t) before each capture; --at is
88
+ then seconds of the timeline
84
89
  --out <dir> Where frames go. Default .gsap-motion/inspect
85
90
  --json Machine-readable output
91
+ --probe timeline Print the timeline reader for a browser you drive
86
92
  explain <file...> Map an animation: scopes, branches, beats, triggers
87
93
  Give it a URL instead to read the running page
88
94
  --json Machine-readable output
@@ -92,9 +98,13 @@ Commands
92
98
  --scroll <px|sel> Scroll, then watch again
93
99
  --reduced --dark Emulate a preference
94
100
  --mobile 390x844 with touch input
101
+ --viewport 1440x900 The window's size; every report prints the one used
95
102
  --cpu 4 Throttle the CPU by this factor
103
+ --headed A visible Chrome on the GPU, for WebGL pages
96
104
  --out <dir> Save a frame alongside the measurements
97
105
  --json Machine-readable output
106
+ --probe install|read Print the recorder, or its findings, for a browser
107
+ you drive — such as a DevTools MCP server
98
108
  patterns check [file] Check the Patterns section of your ANIMATION.md:
99
109
  the fields each status needs, the uses behind
100
110
  validated, the reviewer behind canonical
@@ -290,7 +300,15 @@ function doctor(args = []) {
290
300
  if (provider) report("info", `No separate copy for ${scope}`);
291
301
  else report("info", `Not installed for ${scope}`, `${command} installs it at ${location}`);
292
302
  } else if (installed.version === bundled.version) {
293
- report("ok", `Installed for ${scope}: ${installed.version}`);
303
+ if (provider) {
304
+ report(
305
+ "warn",
306
+ `Installed for ${scope}: ${installed.version}`,
307
+ `The ${provider} plugin provides it too, so the skill is listed twice. Remove ${location}.`,
308
+ );
309
+ } else {
310
+ report("ok", `Installed for ${scope}: ${installed.version}`);
311
+ }
294
312
  } else {
295
313
  report(
296
314
  "warn",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mehshekari/gsap-motion",
3
- "version": "4.3.0",
3
+ "version": "4.5.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 22+ and nothing else.
6
6
  metadata:
7
- version: "4.3.0"
7
+ version: "4.5.0"
8
8
  verified-gsap: "3.15"
9
9
  verified-gsap-react: "2.1"
10
10
  ---
@@ -75,8 +75,8 @@ Core principles:
75
75
  - **A command, explicit or implied:** load its reference and follow it.
76
76
  - **A description** ("make this feel like Apple", "the hero is boring"): that is
77
77
  `create`. Classify with routing.md's table, then load what that needs.
78
- - **Two commands fit:** ask once. A scroll-driven text reveal is `scroll` and
79
- `text` together, not a coin toss.
78
+ - **Two commands fit:** use both, and say so in a line. A scroll-driven text
79
+ reveal is `scroll` and `text` together.
80
80
  - **`design` stops at the plan.** `build` implements one that was approved, and
81
81
  needs it in hand. `evolve` improves what is there and never adds motion.
82
82
 
@@ -109,7 +109,8 @@ intent → precedent → targets → trigger → motion language → technique
109
109
 
110
110
  ## Output format
111
111
 
112
- For anything beyond a one-line tweak, answer in this shape, each part short:
112
+ Beyond a one-line tweak, use this shape, each part short, unless the user or
113
+ project asks for another:
113
114
 
114
115
  - **Analysis** — what moves, why, on what trigger, and the intent you read it
115
116
  as, with the values that follow.
@@ -160,8 +161,7 @@ These catch what is **silent**. Run them from the project root; `<skill-dir>`
160
161
  is the skill's folder.
161
162
 
162
163
  ```bash
163
- # Leaks, per-event and per-frame tweens, layout and paint properties, loops,
164
- # plugin ids, unregistered plugins, dev tooling. Exit 1 on an error.
164
+ # Silent failures: leaks, per-frame cost, plugins, dev tooling. Exit 1 on error.
165
165
  node <skill-dir>/scripts/audit-gsap.mjs [path...] # default: src
166
166
  node <skill-dir>/scripts/audit-gsap.mjs src --quiet # errors only
167
167
  node <skill-dir>/scripts/audit-gsap.mjs src --json
@@ -171,8 +171,8 @@ node <skill-dir>/scripts/audit-svg.mjs <file.svg>
171
171
  node <skill-dir>/scripts/audit-svg.mjs --morph <a.svg> <b.svg>
172
172
  node <skill-dir>/scripts/audit-svg.mjs --hues <file.svg>
173
173
 
174
- # Both need a Chrome. Watch a page animate; then measure what it did:
175
- # first motion, screen share, seams, motion over text.
174
+ # Both open a Chrome (refine.md: --probe, for yours). Watch a page animate,
175
+ # then measure: first motion, screen share, seams, motion over text.
176
176
  node <skill-dir>/scripts/capture-motion.mjs <url> --at 0,300,900
177
177
  node <skill-dir>/scripts/review-motion.mjs <url> --scroll 2000
178
178
  ```
@@ -27,8 +27,7 @@ must survive one belongs in the layout.
27
27
 
28
28
  ## Scope
29
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.
30
+ As in react.md.
32
31
 
33
32
  ## Server rendering and hydration
34
33
 
@@ -112,12 +112,15 @@ was created inside the body.
112
112
  | The tween starts before the object exists | no early return on a null ref |
113
113
  | Memory grows across route changes | objects created outside JSX, never disposed |
114
114
  | Scroll-driven camera drifts out of sync | a scrub with an ease on a child, instead of `ease: "none"` |
115
+ | One long frame the first time a section scrolls in | shaders compiled on first draw — see [performance.md](../reference/performance.md#webgl-the-first-frame-of-each-material) |
116
+ | Scene stays black | WebGL context lost — see [performance.md](../reference/performance.md#webgl-losing-the-context-and-reading-it-back) |
115
117
 
116
118
  ## What the audit covers
117
119
 
118
120
  The React rules from react.md, since these are `.tsx` files. From 3.2,
119
121
  `useFrame` counts as a per-frame scope, so React state set inside it is reported
120
- as `state-per-event`.
122
+ as `state-per-event`, and a Three vector or colour built in it as
123
+ `allocation-per-frame`.
121
124
 
122
125
  Disposal, GPU cost and the render loop are not checkable — read for them, and
123
126
  measure with [performance.md](../reference/performance.md).
@@ -34,13 +34,18 @@ body re-runs on change) and `revertOnUpdate` (the previous run reverts first).
34
34
 
35
35
  A tween created *outside* the body — in a handler, a promise, a callback — is
36
36
  never reverted. Wrap that function in `contextSafe`, from the same hook, and the
37
- context owns it again. Only wrap functions that *create* animations: playing an
38
- existing timeline creates nothing. Inside `mm.add`, use its own: the callback's
37
+ context owns it — with every tween it makes, killed ones too, until it reverts.
38
+ A frequent handler replays a timeline built in the body, or calls a `quickTo`. Inside `mm.add`, use its own: the callback's
39
39
  second argument, `(context, contextSafe) =>`. `contextSafe` runs during render, so reading a
40
40
  ref inside the callback trips React's "cannot access refs during render" rule —
41
41
  build the timeline in the body, keep it on a ref, and expose a `useCallback`
42
42
  that plays it.
43
43
 
44
+ **Without `@gsap/react`, write it by hand:** a `gsap.context(fn, root)` or
45
+ `gsap.matchMedia()` created in `useLayoutEffect`, its `revert` returned as the
46
+ cleanup. The audit reads that as it reads `useGSAP`. `ctx.add(null, fn)` is
47
+ `contextSafe`; `ctx.add(fn)` runs `fn` at once (verified against gsap 3.15).
48
+
44
49
  ## Where it is torn down
45
50
 
46
51
  `useGSAP` reverts its context on unmount. Tweens, timelines, `ScrollTrigger`,
@@ -98,8 +103,7 @@ onMouseMove = (e) => moveX(e.clientX);
98
103
  ```
99
104
 
100
105
  State is for *what exists* — is the modal open, which tab is active. GSAP owns
101
- *how it moves*. Crossing that line is the most common performance bug in
102
- animated React.
106
+ *how it moves*.
103
107
 
104
108
  ## Page and route changes
105
109
 
@@ -118,7 +122,6 @@ App Router. After a navigation, `ScrollTrigger.refresh()` once it settles.
118
122
  | Two components animate each other | missing `scope`, or a shared hardcoded id |
119
123
  | Hydration mismatch | storage or a media query read during render |
120
124
  | Janky pointer effect | React state in the handler |
121
- | Hover sticks halfway | no `overwrite: "auto"` on the retriggered tween |
122
125
 
123
126
  ## What the audit covers
124
127
 
@@ -115,6 +115,8 @@ is a live GPU context for a page that is gone.
115
115
  | Memory grows on every visit | geometries, materials or textures never disposed |
116
116
  | The camera snaps back every frame | `lookAt` overwriting a tweened rotation |
117
117
  | The page is slow on phones before anything moves | Three in the initial bundle instead of imported dynamically |
118
+ | One long frame the first time a section scrolls in | shaders compiled on first draw — see [performance.md](../reference/performance.md#webgl-the-first-frame-of-each-material) |
119
+ | Scene stays black | WebGL context lost — see [performance.md](../reference/performance.md#webgl-losing-the-context-and-reading-it-back) |
118
120
 
119
121
  ## Cost and accessibility
120
122
 
@@ -1,101 +1,101 @@
1
- # Example: magnetic CTA
2
-
3
- A worked interaction, end to end, showing the shape of the answer this skill
4
- should produce.
5
-
6
- ## Analysis
7
-
8
- - **Animating** — a primary CTA and its label.
9
- - **Why** — the button should feel like it wants to be pressed. The response
10
- begins before the cursor arrives, which reads as intent rather than as a
11
- hover state.
12
- - **Style** — premium and restrained. Small travel, long ease, no overshoot.
13
- - **Trigger** — pointer movement inside a padded zone; leave returns it.
14
- - **Techniques** — `gsap.quickTo` for the tracking, a paused timeline for the
15
- hover state. No plugins. MotionPath and Draggable would both be wrong here.
16
-
17
- ## Motion strategy
18
-
19
- 1. The pointer enters the zone — a 32px halo around the button.
20
- 2. The button eases toward the cursor at 32% of the offset.
21
- 3. The label travels half again as far, so the face has depth.
22
- 4. Simultaneously the button scales to 1.04 and its shadow deepens.
23
- 5. On leave, everything eases back to rest over 0.6s.
24
-
25
- The return is the part that makes it feel physical, and the part most
26
- implementations get wrong by snapping.
27
-
28
- ## Architecture
29
-
30
- - `quickTo` functions created once, in the `useGSAP` body — not a tween per
31
- pointer event. This is the whole performance story.
32
- - A paused hover timeline, played and reversed, so the return matches the
33
- arrival without a second definition.
34
- - `gsap.matchMedia` gating on `(hover: hover) and (pointer: fine)` **and**
35
- `prefers-reduced-motion: no-preference`. Both gates are load-bearing.
36
- - Listeners removed in the `matchMedia` callback's own cleanup. `mm` itself
37
- needs no revert: it registers with the active GSAP context and goes with it.
38
-
39
- ## Implementation
40
-
41
- The hook is in [preset/magnetic.md](../preset/magnetic.md) — copy it rather than
42
- duplicating it here.
43
-
44
- ```tsx
45
- "use client";
46
-
47
- import { useMagnetic } from "./useMagnetic";
48
-
49
- export function MagneticCta({
50
- label,
51
- onPress,
52
- }: Readonly<{ label: string; onPress: () => void }>) {
53
- const { zoneRef, targetRef, labelRef } = useMagnetic();
54
-
55
- return (
56
- // The zone's padding is the field radius — the pull starts here, not at
57
- // the button's edge. That is what turns a hover state into a field.
58
- <div ref={zoneRef} className="inline-flex p-8">
59
- <button
60
- ref={targetRef as React.RefObject<HTMLButtonElement>}
61
- type="button"
62
- onClick={onPress}
63
- // The focus ring carries the whole affordance for a keyboard, which
64
- // never sees the pull at all.
65
- className="rounded-full bg-neutral-900 px-7 py-3 text-sm font-medium text-white shadow-lg focus-visible:ring-2"
66
- >
67
- <span ref={labelRef} className="inline-block">
68
- {label}
69
- </span>
70
- </button>
71
- </div>
72
- );
73
- }
74
- ```
75
-
76
- ## Usage
77
-
78
- ```tsx
79
- <MagneticCta label={t("cta.contact")} onPress={open} />
80
- ```
81
-
82
- The label is a message key resolved by the caller.
83
-
84
- ## Notes
85
-
86
- - **Responsive** — the effect does not exist on touch. The button is an
87
- ordinary button there, which is correct; nothing is lost.
88
- - **Performance** — two `quickTo` functions, no allocation per event, only
89
- transforms. The rect is measured per move because scroll shifts it; if that
90
- shows in a profile, cache on enter and invalidate on scroll.
91
- - **Reduced motion** — no pull, no scale. The hover and focus states remain as
92
- instant colour and shadow changes, so the affordance survives.
93
- - **Accessibility** — focus state equals hover state. The transform is small
94
- enough that the focus ring stays under the element.
95
- - **Dependencies** — none beyond `gsap` and `@gsap/react`.
96
-
97
- ## Related
98
-
99
- [preset/magnetic.md](../preset/magnetic.md) ·
100
- [reference/interaction.md](../reference/interaction.md) ·
101
- [reference/performance.md](../reference/performance.md)
1
+ # Example: magnetic CTA
2
+
3
+ A worked interaction, end to end, showing the shape of the answer this skill
4
+ should produce.
5
+
6
+ ## Analysis
7
+
8
+ - **Animating** — a primary CTA and its label.
9
+ - **Why** — the button should feel like it wants to be pressed. The response
10
+ begins before the cursor arrives, which reads as intent rather than as a
11
+ hover state.
12
+ - **Style** — premium and restrained. Small travel, long ease, no overshoot.
13
+ - **Trigger** — pointer movement inside a padded zone; leave returns it.
14
+ - **Techniques** — `gsap.quickTo` for the tracking, a paused timeline for the
15
+ hover state. No plugins. MotionPath and Draggable would both be wrong here.
16
+
17
+ ## Motion strategy
18
+
19
+ 1. The pointer enters the zone — a 32px halo around the button.
20
+ 2. The button eases toward the cursor at 32% of the offset.
21
+ 3. The label travels half again as far, so the face has depth.
22
+ 4. Simultaneously the button scales to 1.04 and its shadow deepens.
23
+ 5. On leave, everything eases back to rest over 0.6s.
24
+
25
+ The return is the part that makes it feel physical, and the part most
26
+ implementations get wrong by snapping.
27
+
28
+ ## Architecture
29
+
30
+ - `quickTo` functions created once, in the `useGSAP` body — not a tween per
31
+ pointer event. This is the whole performance story.
32
+ - A paused hover timeline, played and reversed, so the return matches the
33
+ arrival without a second definition.
34
+ - `gsap.matchMedia` gating on `(hover: hover) and (pointer: fine)` **and**
35
+ `prefers-reduced-motion: no-preference`. Both gates are load-bearing.
36
+ - Listeners removed in the `matchMedia` callback's own cleanup. `mm` itself
37
+ needs no revert: it registers with the active GSAP context and goes with it.
38
+
39
+ ## Implementation
40
+
41
+ The hook is in [preset/magnetic.md](../preset/magnetic.md) — copy it rather than
42
+ duplicating it here.
43
+
44
+ ```tsx
45
+ "use client";
46
+
47
+ import { useMagnetic } from "./useMagnetic";
48
+
49
+ export function MagneticCta({
50
+ label,
51
+ onPress,
52
+ }: Readonly<{ label: string; onPress: () => void }>) {
53
+ const { zoneRef, targetRef, labelRef } = useMagnetic();
54
+
55
+ return (
56
+ // The zone's padding is the field radius — the pull starts here, not at
57
+ // the button's edge. That is what turns a hover state into a field.
58
+ <div ref={zoneRef} className="inline-flex p-8">
59
+ <button
60
+ ref={targetRef as React.RefObject<HTMLButtonElement>}
61
+ type="button"
62
+ onClick={onPress}
63
+ // The focus ring carries the whole affordance for a keyboard, which
64
+ // never sees the pull at all.
65
+ className="rounded-full bg-neutral-900 px-7 py-3 text-sm font-medium text-white shadow-lg focus-visible:ring-2"
66
+ >
67
+ <span ref={labelRef} className="inline-block">
68
+ {label}
69
+ </span>
70
+ </button>
71
+ </div>
72
+ );
73
+ }
74
+ ```
75
+
76
+ ## Usage
77
+
78
+ ```tsx
79
+ <MagneticCta label={t("cta.contact")} onPress={open} />
80
+ ```
81
+
82
+ The label is a message key resolved by the caller.
83
+
84
+ ## Notes
85
+
86
+ - **Responsive** — the effect does not exist on touch. The button is an
87
+ ordinary button there, which is correct; nothing is lost.
88
+ - **Performance** — two `quickTo` functions, no allocation per event, only
89
+ transforms. The rect is measured per move because scroll shifts it; if that
90
+ shows in a profile, cache on enter and invalidate on scroll.
91
+ - **Reduced motion** — no pull, no scale. The hover and focus states remain as
92
+ instant colour and shadow changes, so the affordance survives.
93
+ - **Accessibility** — focus state equals hover state. The transform is small
94
+ enough that the focus ring stays under the element.
95
+ - **Dependencies** — none beyond `gsap` and `@gsap/react`.
96
+
97
+ ## Related
98
+
99
+ [preset/magnetic.md](../preset/magnetic.md) ·
100
+ [reference/interaction.md](../reference/interaction.md) ·
101
+ [reference/performance.md](../reference/performance.md)