@polymorphism-tech/morph-spec 4.16.0 → 4.17.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 +2 -2
- package/bin/morph-spec.js +3 -0
- package/bin/task-manager.js +999 -962
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/agents/ai-agents/vector-search-expert.md +247 -233
- package/framework/agents/backend/ms-agent-expert.md +1 -1
- package/framework/agents/backend/neon-expert.md +397 -0
- package/framework/agents/frontend/gsap-animator.md +131 -0
- package/framework/agents.json +1175 -1085
- package/framework/commands/morph-preflight.md +200 -192
- package/framework/hooks/claude-code/session-start/inject-morph-context.js +1 -1
- package/framework/rules/gsap-standards.md +13 -0
- package/framework/rules/neon-standards.md +25 -0
- package/framework/skills/level-0-meta/mcp-registry.json +1 -1
- package/framework/skills/level-0-meta/morph-agent-teams/SKILL.md +472 -472
- package/framework/skills/level-0-meta/morph-init/SKILL.md +11 -11
- package/framework/skills/level-0-meta/morph-post-implementation/SKILL.md +5 -5
- package/framework/skills/level-0-meta/morph-tool-usage-guide/SKILL.md +5 -5
- package/framework/skills/level-0-meta/morph-tool-usage-guide/references/tools-per-phase.md +7 -7
- package/framework/skills/level-1-workflows/morph-phase-codebase-analysis/SKILL.md +24 -24
- package/framework/skills/level-1-workflows/morph-phase-design/SKILL.md +12 -12
- package/framework/skills/level-1-workflows/morph-phase-implement/SKILL.md +9 -9
- package/framework/skills/level-1-workflows/morph-phase-plan/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/morph-phase-tasks/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/morph-phase-tasks/references/task-planning-patterns.md +143 -143
- package/framework/skills/level-1-workflows/morph-phase-uiux/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/morph-scope-escalation/SKILL.md +1 -1
- package/framework/standards/STANDARDS.json +184 -107
- package/framework/standards/ai-agents/production.md +1 -0
- package/framework/standards/ai-agents/setup.md +3 -2
- package/framework/standards/ai-agents/workflows.md +1 -0
- package/framework/standards/architecture/vertical-slice/vertical-slice.md +4 -4
- package/framework/standards/backend/database/postgresql/database.md +58 -78
- package/framework/standards/backend/dotnet/scalar-ui.md +172 -172
- package/framework/standards/backend/dotnet/vsa-handler-patterns.md +274 -274
- package/framework/standards/backend/integrations/asaas/asaas-api.md +1 -1
- package/framework/standards/backend/integrations/clerk/clerk-auth.md +23 -21
- package/framework/standards/frontend/design-system/animations.md +9 -0
- package/framework/standards/frontend/gsap/gsap-core.md +463 -0
- package/framework/standards/frontend/gsap/gsap-plugins.md +294 -0
- package/framework/standards/frontend/gsap/gsap-scrolltrigger.md +318 -0
- package/framework/standards/frontend/nextjs/motion-patterns.md +1 -0
- package/framework/standards/frontend/nextjs/nextjs-patterns.md +47 -37
- package/framework/standards/infrastructure/docker/easypanel-deploy.md +4 -8
- package/framework/standards/infrastructure/neon/neon-pgrag.md +239 -0
- package/framework/standards/infrastructure/neon/neon-pgvector.md +236 -0
- package/framework/standards/infrastructure/neon/neon-semantic-kernel.md +310 -0
- package/framework/standards/infrastructure/neon/neon-setup.md +186 -0
- package/framework/standards/integration/mcp/mcp-tools.md +32 -34
- package/framework/templates/README.md +18 -18
- package/framework/templates/REGISTRY.json +49 -96
- package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
- package/framework/templates/code/dotnet/backend/repository.cs +148 -148
- package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +2 -2
- package/framework/templates/code/dotnet/database/vector-entity.cs +93 -0
- package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +1 -1
- package/framework/templates/code/sql/{supabase-migration.sql → postgresql-migration.sql} +8 -17
- package/framework/templates/code/sql/{supabase-migration.template.sql → postgresql-migration.template.sql} +10 -13
- package/framework/templates/code/sql/{rls-policy.sql → postgresql-rls-policy.sql} +8 -8
- package/framework/templates/code/typescript/contracts.ts +167 -168
- package/framework/templates/context/CONTEXT.md +3 -3
- package/framework/templates/docs/proposal.md +6 -6
- package/framework/templates/docs/ui-components.md +1 -1
- package/framework/templates/feature/decisions.md +121 -121
- package/framework/templates/feature/recap.md +145 -145
- package/framework/templates/feature/tasks.md +199 -199
- package/framework/templates/frontend/nextjs/gsap-scroll-section.tsx.hbs +44 -0
- package/framework/templates/infrastructure/docker/docker-compose.template.yml +50 -67
- package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
- package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
- package/framework/templates/infrastructure/docker/easypanel.template.json +46 -54
- package/framework/templates/infrastructure/github/README.md +56 -56
- package/framework/templates/integrations/semantic-kernel-neon.cs +125 -0
- package/framework/workflows/README.md +1 -1
- package/framework/workflows/docs/full-morph.md +69 -69
- package/package.json +1 -1
- package/src/commands/agents/dispatch-agents.js +19 -15
- package/src/commands/state/advance-phase.js +5 -0
- package/src/commands/tasks/task.js +3 -0
- package/src/core/state/state-manager.js +45 -1
- package/src/core/templates/template-registry.js +1 -66
- package/src/lib/detectors/index.js +2 -2
- package/src/lib/generators/architecture-scanner.js +640 -640
- package/src/lib/installers/mcp-installer.js +1 -1
- package/src/lib/tasks/task-classifier.js +15 -1
- package/src/lib/tasks/test-runner.js +5 -1
- package/src/lib/validators/content/content-validator.js +9 -10
- package/src/lib/validators/validation-runner.js +7 -5
- package/framework/agents/backend/supabase-expert.md +0 -256
- package/framework/rules/supabase-standards.md +0 -29
- package/framework/standards/infrastructure/supabase/mcp-setup.md +0 -142
- package/framework/standards/infrastructure/supabase/supabase-auth.md +0 -176
- package/framework/standards/infrastructure/supabase/supabase-custom-jwt.md +0 -206
- package/framework/standards/infrastructure/supabase/supabase-pgvector.md +0 -169
- package/framework/standards/infrastructure/supabase/supabase-rls.md +0 -184
- package/framework/standards/infrastructure/supabase/supabase-storage.md +0 -153
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
---
|
|
2
|
+
scope: frontend-nextjs-gsap
|
|
3
|
+
layer: 3
|
|
4
|
+
keywords: flip, splittext, morphsvg, drawsvg, motionpath, draggable, observer, scrollto, gsap-plugins
|
|
5
|
+
load_when: gsap-animator active
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# GSAP Plugins
|
|
9
|
+
|
|
10
|
+
Reference guide for GSAP's plugin ecosystem in Next.js. Covers free plugins (Flip, Observer, ScrollToPlugin) and Club GreenSock premium plugins (SplitText, MorphSVG, DrawSVG, MotionPath, Draggable, InertiaPlugin).
|
|
11
|
+
|
|
12
|
+
## Plugin Registration & Licensing
|
|
13
|
+
|
|
14
|
+
Free plugins (included in gsap 3.12+): Flip, Observer, ScrollToPlugin
|
|
15
|
+
|
|
16
|
+
Premium (Club GreenSock): SplitText, MorphSVG, DrawSVG, MotionPath, Draggable, InertiaPlugin, ScrambleText, CustomEase, CustomWiggle, CustomBounce, Physics2D, PhysicsProps
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# Free plugins — included with gsap
|
|
20
|
+
npm install gsap
|
|
21
|
+
|
|
22
|
+
# Premium plugins — requires Club GreenSock license
|
|
23
|
+
npm install gsap@npm:@gsap/shockingly
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Registration pattern (do ONCE at app init):
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
'use client';
|
|
30
|
+
import gsap from 'gsap';
|
|
31
|
+
import { Flip } from 'gsap/Flip';
|
|
32
|
+
import { SplitText } from 'gsap/SplitText';
|
|
33
|
+
import { ScrollToPlugin } from 'gsap/ScrollToPlugin';
|
|
34
|
+
|
|
35
|
+
gsap.registerPlugin(Flip, SplitText, ScrollToPlugin);
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Flip Plugin (Free)
|
|
39
|
+
|
|
40
|
+
Layout transition animations using the FLIP technique (First, Last, Invert, Play):
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
'use client';
|
|
44
|
+
import { useGSAP } from '@gsap/react';
|
|
45
|
+
import gsap from 'gsap';
|
|
46
|
+
import { Flip } from 'gsap/Flip';
|
|
47
|
+
|
|
48
|
+
function FilterableGrid({ items, filter }: { items: Item[]; filter: string }) {
|
|
49
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
50
|
+
|
|
51
|
+
const applyFilter = (newFilter: string) => {
|
|
52
|
+
// 1. Capture current state
|
|
53
|
+
const state = Flip.getState('.grid-item');
|
|
54
|
+
|
|
55
|
+
// 2. Make DOM changes (reorder, show/hide)
|
|
56
|
+
setFilter(newFilter);
|
|
57
|
+
|
|
58
|
+
// 3. Animate from old positions to new
|
|
59
|
+
Flip.from(state, {
|
|
60
|
+
duration: 0.6,
|
|
61
|
+
ease: 'power2.out',
|
|
62
|
+
stagger: 0.04,
|
|
63
|
+
absolute: true, // prevents layout jumping during animation
|
|
64
|
+
onEnter: (elements) => gsap.fromTo(elements, { opacity: 0, scale: 0.8 }, { opacity: 1, scale: 1, duration: 0.4 }),
|
|
65
|
+
onLeave: (elements) => gsap.to(elements, { opacity: 0, scale: 0.8, duration: 0.3 }),
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Use cases: filterable grids, list reordering, expanding cards, shared element transitions.
|
|
72
|
+
|
|
73
|
+
## SplitText (Premium)
|
|
74
|
+
|
|
75
|
+
Split text into chars, words, or lines for staggered reveals:
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
useGSAP(() => {
|
|
79
|
+
const split = SplitText.create('.hero-title', {
|
|
80
|
+
type: 'chars,words',
|
|
81
|
+
autoSplit: true, // re-splits on resize
|
|
82
|
+
onSplit: () => {
|
|
83
|
+
// Animate after split is ready
|
|
84
|
+
gsap.from(split.chars, {
|
|
85
|
+
y: 30,
|
|
86
|
+
opacity: 0,
|
|
87
|
+
stagger: 0.03,
|
|
88
|
+
duration: 0.5,
|
|
89
|
+
ease: 'power2.out',
|
|
90
|
+
});
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Cleanup: revert split on unmount (handled by useGSAP context)
|
|
95
|
+
}, { scope: containerRef });
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Key points:
|
|
99
|
+
- Use `autoSplit: true` for responsive -- re-splits when container resizes
|
|
100
|
+
- Use `onSplit` callback to animate AFTER split completes
|
|
101
|
+
- The split creates `<div>` wrappers -- this affects layout. Test carefully.
|
|
102
|
+
- useGSAP automatically reverts the split on unmount
|
|
103
|
+
|
|
104
|
+
## MorphSVG (Premium)
|
|
105
|
+
|
|
106
|
+
Morph between SVG shapes:
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
gsap.to('#circle', {
|
|
110
|
+
morphSVG: {
|
|
111
|
+
shape: '#star',
|
|
112
|
+
type: 'rotational', // smoother for complex shapes
|
|
113
|
+
shapeIndex: 'auto', // auto-aligns anchor points
|
|
114
|
+
},
|
|
115
|
+
duration: 1.2,
|
|
116
|
+
ease: 'power2.inOut',
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Convert non-path SVG elements to paths first
|
|
120
|
+
MorphSVGPlugin.convertToPath('circle, rect, ellipse, polygon, polyline, line');
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Rules:
|
|
124
|
+
- Both shapes must be `<path>` elements (use convertToPath for others)
|
|
125
|
+
- Match point counts for smoother morphs
|
|
126
|
+
- `type: "rotational"` is smoother for most shapes
|
|
127
|
+
|
|
128
|
+
## DrawSVG (Premium)
|
|
129
|
+
|
|
130
|
+
Animate SVG stroke drawing:
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
// Draw stroke from 0% to 100%
|
|
134
|
+
gsap.fromTo('.svg-path',
|
|
135
|
+
{ drawSVG: '0%' },
|
|
136
|
+
{ drawSVG: '100%', duration: 2, ease: 'power2.inOut' }
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// Partial draws
|
|
140
|
+
gsap.to('.line', { drawSVG: '20% 80%', duration: 1 }); // middle section only
|
|
141
|
+
|
|
142
|
+
// Line drawing reveal pattern
|
|
143
|
+
const paths = gsap.utils.toArray<SVGPathElement>('.icon path');
|
|
144
|
+
gsap.from(paths, {
|
|
145
|
+
drawSVG: '0%',
|
|
146
|
+
stagger: 0.2,
|
|
147
|
+
duration: 1,
|
|
148
|
+
ease: 'power1.out',
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Requires SVG elements with visible `stroke` and `stroke-width`. Does not work on `fill`.
|
|
153
|
+
|
|
154
|
+
## MotionPath (Premium)
|
|
155
|
+
|
|
156
|
+
Animate elements along SVG paths:
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
gsap.to('.rocket', {
|
|
160
|
+
motionPath: {
|
|
161
|
+
path: '#flight-path',
|
|
162
|
+
align: '#flight-path',
|
|
163
|
+
autoRotate: true, // element rotates to follow path direction
|
|
164
|
+
alignOrigin: [0.5, 0.5], // center of element on path
|
|
165
|
+
},
|
|
166
|
+
duration: 3,
|
|
167
|
+
ease: 'power1.inOut',
|
|
168
|
+
});
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Use MotionPathHelper (dev tool) to visually edit paths: `MotionPathHelper.create('.rocket')`.
|
|
172
|
+
|
|
173
|
+
## ScrollToPlugin (Free)
|
|
174
|
+
|
|
175
|
+
Smooth scroll to elements or positions:
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
// Scroll to element
|
|
179
|
+
gsap.to(window, {
|
|
180
|
+
scrollTo: { y: '#contact-section', offsetY: 80 }, // 80px offset for header
|
|
181
|
+
duration: 1,
|
|
182
|
+
ease: 'power2.inOut',
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Scroll to position
|
|
186
|
+
gsap.to(window, { scrollTo: { y: 500 }, duration: 0.8 });
|
|
187
|
+
|
|
188
|
+
// Auto-kill on user scroll (prevents fighting user input)
|
|
189
|
+
gsap.to(window, {
|
|
190
|
+
scrollTo: { y: '#section', autoKill: true },
|
|
191
|
+
duration: 1.5,
|
|
192
|
+
});
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Draggable (Premium)
|
|
196
|
+
|
|
197
|
+
Make elements draggable with optional physics:
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
'use client';
|
|
201
|
+
import { useGSAP } from '@gsap/react';
|
|
202
|
+
import gsap from 'gsap';
|
|
203
|
+
import { Draggable } from 'gsap/Draggable';
|
|
204
|
+
|
|
205
|
+
function DraggableCard() {
|
|
206
|
+
const cardRef = useRef<HTMLDivElement>(null);
|
|
207
|
+
|
|
208
|
+
useGSAP(() => {
|
|
209
|
+
Draggable.create(cardRef.current, {
|
|
210
|
+
type: 'x,y',
|
|
211
|
+
bounds: '.container',
|
|
212
|
+
edgeResistance: 0.65,
|
|
213
|
+
throwProps: true, // momentum (requires InertiaPlugin)
|
|
214
|
+
snap: {
|
|
215
|
+
x: (value) => Math.round(value / 100) * 100, // snap to 100px grid
|
|
216
|
+
y: (value) => Math.round(value / 100) * 100,
|
|
217
|
+
},
|
|
218
|
+
onDrag() { /* called every frame during drag */ },
|
|
219
|
+
onDragEnd() { /* called when drag completes */ },
|
|
220
|
+
});
|
|
221
|
+
}, { scope: cardRef });
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Observer (Free)
|
|
226
|
+
|
|
227
|
+
Normalize pointer, scroll, touch, and wheel events:
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
import { Observer } from 'gsap/Observer';
|
|
231
|
+
|
|
232
|
+
Observer.create({
|
|
233
|
+
target: window,
|
|
234
|
+
type: 'wheel,touch,pointer',
|
|
235
|
+
onUp: () => goToSection(currentIndex - 1),
|
|
236
|
+
onDown: () => goToSection(currentIndex + 1),
|
|
237
|
+
tolerance: 10, // minimum distance before triggering
|
|
238
|
+
preventDefault: true, // prevent native scroll
|
|
239
|
+
wheelSpeed: -1, // invert direction
|
|
240
|
+
});
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Use cases: custom scroll-jacking, swipe detection, directional gestures, fullpage scroll experiences.
|
|
244
|
+
|
|
245
|
+
## Combining Plugins — Real-World Recipes
|
|
246
|
+
|
|
247
|
+
**Recipe 1: Scroll-triggered text reveal**
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
useGSAP(() => {
|
|
251
|
+
const split = SplitText.create('.reveal-text', { type: 'words', autoSplit: true });
|
|
252
|
+
|
|
253
|
+
gsap.from(split.words, {
|
|
254
|
+
y: 20,
|
|
255
|
+
opacity: 0,
|
|
256
|
+
stagger: 0.05,
|
|
257
|
+
duration: 0.5,
|
|
258
|
+
ease: 'power2.out',
|
|
259
|
+
scrollTrigger: {
|
|
260
|
+
trigger: '.reveal-text',
|
|
261
|
+
start: 'top 80%',
|
|
262
|
+
toggleActions: 'play none none none',
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
}, { scope: containerRef });
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
**Recipe 2: Filterable grid with Flip + ScrollTrigger.refresh()**
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
const handleFilter = (category: string) => {
|
|
272
|
+
const state = Flip.getState('.item');
|
|
273
|
+
setActiveCategory(category);
|
|
274
|
+
|
|
275
|
+
Flip.from(state, {
|
|
276
|
+
duration: 0.5,
|
|
277
|
+
stagger: 0.03,
|
|
278
|
+
absolute: true,
|
|
279
|
+
onComplete: () => ScrollTrigger.refresh(), // recalculate positions after layout change
|
|
280
|
+
});
|
|
281
|
+
};
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
**Recipe 3: SVG icon draw + morph sequence**
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
const tl = gsap.timeline({ scrollTrigger: { trigger: '.icon-section', start: 'top center' } });
|
|
288
|
+
tl.from('.icon path', { drawSVG: '0%', stagger: 0.1, duration: 0.8 })
|
|
289
|
+
.to('.icon path', { morphSVG: '#final-shape', duration: 1, ease: 'power2.inOut' }, '+=0.3');
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
*MORPH-SPEC by Polymorphism Tech*
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
---
|
|
2
|
+
scope: frontend-nextjs-gsap
|
|
3
|
+
layer: 3
|
|
4
|
+
keywords: scrolltrigger, scroll-animation, pinning, scrubbing, parallax, scroll-driven, gsap
|
|
5
|
+
load_when: gsap-animator active
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# GSAP ScrollTrigger — Next.js
|
|
9
|
+
|
|
10
|
+
Reference guide for implementing scroll-driven animations in Next.js projects using GSAP ScrollTrigger. Covers plugin setup, scrubbing, pinning, batch processing, responsive breakpoints, SSR considerations, and mandatory cleanup patterns.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Section 1: Plugin Registration
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
'use client';
|
|
18
|
+
import gsap from 'gsap';
|
|
19
|
+
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
|
20
|
+
|
|
21
|
+
gsap.registerPlugin(ScrollTrigger);
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Register ONCE at app init. In Next.js, do this in a client-side providers file or at the top of client components. Never register inside `useEffect` or loops — repeated registration is wasted work and can cause subtle timing issues.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Section 2: Basic ScrollTrigger
|
|
29
|
+
|
|
30
|
+
The fundamental pattern: attach a `scrollTrigger` object to any GSAP tween.
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
useGSAP(() => {
|
|
34
|
+
gsap.from('.card', {
|
|
35
|
+
y: 60,
|
|
36
|
+
opacity: 0,
|
|
37
|
+
duration: 0.8,
|
|
38
|
+
ease: 'power2.out',
|
|
39
|
+
stagger: 0.15,
|
|
40
|
+
scrollTrigger: {
|
|
41
|
+
trigger: '.cards-container',
|
|
42
|
+
start: 'top 80%', // trigger-top hits viewport-80%
|
|
43
|
+
end: 'bottom 20%',
|
|
44
|
+
toggleActions: 'play none none none', // onEnter onLeave onEnterBack onLeaveBack
|
|
45
|
+
markers: false, // true for debugging only
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
}, { scope: containerRef });
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**start/end syntax:** `"triggerPosition viewportPosition"`.
|
|
52
|
+
|
|
53
|
+
| Value | Meaning |
|
|
54
|
+
|-------|---------|
|
|
55
|
+
| `"top center"` | Trigger element's top reaches viewport center |
|
|
56
|
+
| `"top 80%"` | Trigger element's top reaches 80% down the viewport |
|
|
57
|
+
| `"top top"` | Trigger element's top reaches viewport top |
|
|
58
|
+
| `"bottom top"` | Trigger element's bottom reaches viewport top |
|
|
59
|
+
|
|
60
|
+
**toggleActions** accepts four space-separated values for `onEnter`, `onLeave`, `onEnterBack`, `onLeaveBack`. Each value can be: `play`, `pause`, `resume`, `reverse`, `restart`, `reset`, `complete`, `none`.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Section 3: Scrubbing
|
|
65
|
+
|
|
66
|
+
Link animation progress directly to scroll position instead of playing on a timeline.
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
gsap.to('.parallax-bg', {
|
|
70
|
+
y: -200,
|
|
71
|
+
ease: 'none', // linear for scrub
|
|
72
|
+
scrollTrigger: {
|
|
73
|
+
trigger: '.hero-section',
|
|
74
|
+
start: 'top top',
|
|
75
|
+
end: 'bottom top',
|
|
76
|
+
scrub: true, // direct link (can feel jumpy)
|
|
77
|
+
// scrub: 0.5, // smooth scrub with 0.5s delay
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Rule:** Use `ease: "none"` with scrub for linear scroll-to-progress mapping. Non-linear eases with scrub cause unexpected acceleration/deceleration that fights the user's scroll intent.
|
|
83
|
+
|
|
84
|
+
- `scrub: true` — instant 1:1 link between scroll and progress.
|
|
85
|
+
- `scrub: 0.5` — smoothed with 0.5 second catch-up delay. Use values between 0.3 and 1 for production.
|
|
86
|
+
- `scrub: 3` — heavy smoothing, good for background parallax layers.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Section 4: Pinning
|
|
91
|
+
|
|
92
|
+
Pin an element in place while the user scrolls through content.
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
gsap.to('.pinned-content', {
|
|
96
|
+
x: '-300%', // horizontal scroll simulation
|
|
97
|
+
ease: 'none',
|
|
98
|
+
scrollTrigger: {
|
|
99
|
+
trigger: '.horizontal-section',
|
|
100
|
+
start: 'top top',
|
|
101
|
+
end: '+=3000', // pin for 3000px of scroll
|
|
102
|
+
pin: true,
|
|
103
|
+
scrub: 1,
|
|
104
|
+
anticipatePin: 1, // prevents jank on pin start
|
|
105
|
+
// pinSpacing: true, // default: adds space for pinned section
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Horizontal scroll pattern:** Pin the outer container, then translate an inner track of panels by `-100% * (panelCount - 1)` using scrub. The `end: "+=Npx"` controls how much vertical scroll maps to the horizontal distance.
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
useGSAP(() => {
|
|
114
|
+
const panels = gsap.utils.toArray<HTMLElement>('.panel');
|
|
115
|
+
|
|
116
|
+
gsap.to(panels, {
|
|
117
|
+
xPercent: -100 * (panels.length - 1),
|
|
118
|
+
ease: 'none',
|
|
119
|
+
scrollTrigger: {
|
|
120
|
+
trigger: '.horizontal-wrapper',
|
|
121
|
+
start: 'top top',
|
|
122
|
+
end: () => `+=${panels.length * window.innerWidth}`,
|
|
123
|
+
pin: true,
|
|
124
|
+
scrub: 1,
|
|
125
|
+
snap: 1 / (panels.length - 1),
|
|
126
|
+
anticipatePin: 1,
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
}, { scope: containerRef });
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**Pinning rules:**
|
|
133
|
+
- Set `anticipatePin: 1` to avoid visual jank when pinning starts.
|
|
134
|
+
- `pinSpacing: false` prevents the extra space insertion — use only when the pinned section overlaps subsequent content intentionally.
|
|
135
|
+
- Never nest pinned elements inside other pinned elements.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Section 5: Batch Processing
|
|
140
|
+
|
|
141
|
+
For staggered reveals on lists and grids, use `ScrollTrigger.batch` instead of individual triggers.
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
ScrollTrigger.batch('.grid-item', {
|
|
145
|
+
onEnter: (elements) => {
|
|
146
|
+
gsap.from(elements, {
|
|
147
|
+
y: 40,
|
|
148
|
+
opacity: 0,
|
|
149
|
+
stagger: 0.08,
|
|
150
|
+
duration: 0.6,
|
|
151
|
+
ease: 'power2.out',
|
|
152
|
+
});
|
|
153
|
+
},
|
|
154
|
+
start: 'top 85%',
|
|
155
|
+
});
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Use batch instead of individual ScrollTriggers for performance — one IntersectionObserver watches all matched elements instead of creating dozens of separate scroll listeners. This matters significantly for grids with 20+ items.
|
|
159
|
+
|
|
160
|
+
Batch also supports `onLeave`, `onEnterBack`, and `onLeaveBack` callbacks with the same signature.
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Section 6: Scroll-linked Progress
|
|
165
|
+
|
|
166
|
+
Use `onUpdate` for custom progress-based effects that go beyond simple tweens.
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
ScrollTrigger.create({
|
|
170
|
+
trigger: '.progress-section',
|
|
171
|
+
start: 'top center',
|
|
172
|
+
end: 'bottom center',
|
|
173
|
+
onUpdate: (self) => {
|
|
174
|
+
// self.progress is 0-1
|
|
175
|
+
gsap.set('.progress-bar', { scaleX: self.progress });
|
|
176
|
+
gsap.set('.counter', { innerText: Math.round(self.progress * 100) + '%' });
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Parallax layers pattern:** Assign different scroll speeds to layered elements using a `data-depth` attribute.
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
gsap.utils.toArray<HTMLElement>('.parallax-layer').forEach((layer) => {
|
|
185
|
+
const depth = parseFloat(layer.dataset.depth || '0.5');
|
|
186
|
+
gsap.to(layer, {
|
|
187
|
+
y: -200 * depth,
|
|
188
|
+
ease: 'none',
|
|
189
|
+
scrollTrigger: {
|
|
190
|
+
trigger: layer.parentElement,
|
|
191
|
+
start: 'top bottom',
|
|
192
|
+
end: 'bottom top',
|
|
193
|
+
scrub: true,
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Layers with `data-depth="1"` move the full -200px, while `data-depth="0.2"` moves only -40px, creating natural depth.
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Section 7: Snap
|
|
204
|
+
|
|
205
|
+
Snap scrolling to discrete sections for a full-page slide experience.
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
ScrollTrigger.create({
|
|
209
|
+
trigger: '.sections-container',
|
|
210
|
+
start: 'top top',
|
|
211
|
+
end: 'bottom bottom',
|
|
212
|
+
snap: {
|
|
213
|
+
snapTo: 1 / (sectionCount - 1), // snap to each section
|
|
214
|
+
duration: { min: 0.2, max: 0.6 },
|
|
215
|
+
ease: 'power1.inOut',
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
- `snapTo` accepts a number (interval), an array of progress values, or a function `(value) => snappedValue`.
|
|
221
|
+
- `duration` can be a fixed number or `{ min, max }` range — GSAP picks based on scroll velocity.
|
|
222
|
+
- Keep snap durations short (under 0.8s) to avoid fighting user intent.
|
|
223
|
+
- Combine with `pin: true` for pinned section snapping.
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## Section 8: Responsive
|
|
228
|
+
|
|
229
|
+
Use `ScrollTrigger.matchMedia()` for breakpoint-aware animations.
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
ScrollTrigger.matchMedia({
|
|
233
|
+
// desktop
|
|
234
|
+
'(min-width: 768px)': () => {
|
|
235
|
+
gsap.to('.sidebar', {
|
|
236
|
+
scrollTrigger: { trigger: '.content', pin: true, scrub: true },
|
|
237
|
+
y: -100,
|
|
238
|
+
});
|
|
239
|
+
},
|
|
240
|
+
// mobile — different or no animation
|
|
241
|
+
'(max-width: 767px)': () => {
|
|
242
|
+
gsap.set('.sidebar', { clearProps: 'all' });
|
|
243
|
+
},
|
|
244
|
+
// all: always active
|
|
245
|
+
all: () => {
|
|
246
|
+
// common setup shared across breakpoints
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Animations created inside each breakpoint callback are automatically reverted when the media query no longer matches, then re-created if the query matches again. This prevents stale transforms from desktop persisting on mobile after a resize.
|
|
252
|
+
|
|
253
|
+
**Rules:**
|
|
254
|
+
- Always define both desktop and mobile cases explicitly. Omitting mobile leaves desktop transforms applied.
|
|
255
|
+
- Place `matchMedia` inside `useGSAP` so it is cleaned up on unmount.
|
|
256
|
+
- Avoid overlapping breakpoint ranges — use exclusive boundaries (767px / 768px, not both at 768px).
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## Section 9: Next.js SSR Considerations
|
|
261
|
+
|
|
262
|
+
ScrollTrigger relies on the DOM, `window`, and scroll position — it cannot run server-side.
|
|
263
|
+
|
|
264
|
+
**Mandatory rules for Next.js:**
|
|
265
|
+
|
|
266
|
+
1. All ScrollTrigger code must live in `'use client'` components.
|
|
267
|
+
2. Use the `useGSAP()` hook, which only executes client-side after mount. Never use bare `useEffect` for GSAP unless wrapping in `gsap.context()`.
|
|
268
|
+
3. Call `ScrollTrigger.refresh()` after any event that changes layout dimensions:
|
|
269
|
+
- Dynamic content loads (API data arriving, skeleton replaced with real content)
|
|
270
|
+
- Layout shifts (accordion opens, tab changes, collapsible sections)
|
|
271
|
+
- Route changes in Next.js App Router
|
|
272
|
+
- Images finishing load
|
|
273
|
+
4. For Next.js Image components, use the `onLoad` callback to trigger a refresh:
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
<Image
|
|
277
|
+
src="/hero.jpg"
|
|
278
|
+
alt="Hero"
|
|
279
|
+
onLoad={() => ScrollTrigger.refresh()}
|
|
280
|
+
/>
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
5. Use `ScrollTrigger.normalizeScroll(true)` for consistent cross-browser scroll behavior. This is particularly useful on iOS Safari where elastic bouncing and address bar resizing interfere with scroll calculations.
|
|
284
|
+
6. Avoid `ScrollTrigger.create()` at module scope — always place it inside `useGSAP` or a `useEffect` to guarantee the DOM exists.
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## Section 10: Cleanup
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
// Automatic with useGSAP — kills all animations and ScrollTriggers created in scope
|
|
292
|
+
useGSAP(() => {
|
|
293
|
+
// all ScrollTriggers here are auto-cleaned on unmount
|
|
294
|
+
}, { scope: containerRef });
|
|
295
|
+
|
|
296
|
+
// Manual cleanup (if not using useGSAP):
|
|
297
|
+
useEffect(() => {
|
|
298
|
+
const ctx = gsap.context(() => {
|
|
299
|
+
// animations + ScrollTriggers
|
|
300
|
+
}, containerRef);
|
|
301
|
+
return () => ctx.revert(); // kills everything
|
|
302
|
+
}, []);
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
**CRITICAL:** Always clean up ScrollTriggers on unmount. Orphaned ScrollTriggers cause memory leaks, stale callbacks, and broken scroll behavior on subsequent route navigations.
|
|
306
|
+
|
|
307
|
+
**Refresh priority** for ordered triggers — higher priority triggers refresh first:
|
|
308
|
+
|
|
309
|
+
```typescript
|
|
310
|
+
ScrollTrigger.create({ trigger: '.first', refreshPriority: 1 });
|
|
311
|
+
ScrollTrigger.create({ trigger: '.second', refreshPriority: -1 });
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Use `refreshPriority` when one trigger's position depends on another's pinning or layout effect. The default priority is 0; higher values refresh first.
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
*MORPH-SPEC by Polymorphism Tech*
|
|
@@ -299,6 +299,7 @@ This CSS fallback catches animations outside Framer Motion (Tailwind `animate-*`
|
|
|
299
299
|
| Forgetting `animation-fill-mode: backwards` in stagger | Will cause a flash before the animation starts |
|
|
300
300
|
| `AnimatePresence` without `key` changes | Exit animations will never fire |
|
|
301
301
|
| Using `framer-motion` for skeleton loaders | Use CSS `animate-pulse` — lower performance cost |
|
|
302
|
+
| Need ScrollTrigger, SVG morph, or complex timelines | Use GSAP — see `frontend/gsap/gsap-core.md` |
|
|
302
303
|
|
|
303
304
|
---
|
|
304
305
|
|