@veyralabs/webcloner 0.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.
- package/README.md +115 -0
- package/SKILL.md +565 -0
- package/package.json +39 -0
- package/references/animation-playbook.md +292 -0
- package/references/behavior-spec-format.md +259 -0
- package/references/component-detection.md +209 -0
- package/references/stack-presets.md +328 -0
- package/scripts/compare.mjs +87 -0
- package/scripts/download-assets.mjs +160 -0
- package/scripts/extract.py +344 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
# Animation Playbook — WebCloner Reference
|
|
2
|
+
|
|
3
|
+
How to detect, extract, and recreate animations from the 5 most common animation libraries.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. CSS Animations (no library)
|
|
8
|
+
|
|
9
|
+
**Detection:** `manifest.animations.libraries` is empty, but `@keyframes` exist in stylesheets.
|
|
10
|
+
|
|
11
|
+
**Extraction from manifest:**
|
|
12
|
+
Look for `animation` or `transition` properties in element `styles`. Example:
|
|
13
|
+
```
|
|
14
|
+
"animation": "fadeIn 0.6s ease-out forwards"
|
|
15
|
+
"transition": "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
**Recreation:**
|
|
19
|
+
Copy `@keyframes` from the original stylesheet into `globals.css`. Apply via Tailwind's `animate-*`
|
|
20
|
+
or direct CSS class.
|
|
21
|
+
|
|
22
|
+
```css
|
|
23
|
+
@keyframes fadeIn {
|
|
24
|
+
from { opacity: 0; transform: translateY(20px); }
|
|
25
|
+
to { opacity: 1; transform: translateY(0); }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.animate-fade-in {
|
|
29
|
+
animation: fadeIn 0.6s ease-out forwards;
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Scroll-triggered CSS animations:**
|
|
34
|
+
If `IntersectionObserver` is driving class toggles, recreate with:
|
|
35
|
+
```typescript
|
|
36
|
+
'use client';
|
|
37
|
+
import { useInView } from 'react-intersection-observer';
|
|
38
|
+
|
|
39
|
+
const { ref, inView } = useInView({ threshold: 0.2, triggerOnce: true });
|
|
40
|
+
return <div ref={ref} className={inView ? 'animate-fade-in' : 'opacity-0'} />;
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 2. Lenis (smooth scroll)
|
|
46
|
+
|
|
47
|
+
**Detection:** `manifest.animations.libraries` includes `"lenis"`.
|
|
48
|
+
|
|
49
|
+
**What it does:** Replaces native browser scroll with a physics-based smooth scroll.
|
|
50
|
+
Without it, the clone will feel "snappy" vs the original's "buttery" feel.
|
|
51
|
+
|
|
52
|
+
**Installation:**
|
|
53
|
+
```bash
|
|
54
|
+
npm install lenis
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**Recreation:**
|
|
58
|
+
```typescript
|
|
59
|
+
// src/components/LenisProvider.tsx
|
|
60
|
+
'use client';
|
|
61
|
+
import { useEffect } from 'react';
|
|
62
|
+
import Lenis from 'lenis';
|
|
63
|
+
|
|
64
|
+
export function LenisProvider({ children }: { children: React.ReactNode }) {
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
const lenis = new Lenis({
|
|
67
|
+
duration: 1.2, // match original — inspect window.lenis?.options
|
|
68
|
+
easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
|
|
69
|
+
orientation: 'vertical',
|
|
70
|
+
smoothWheel: true,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
function raf(time: number) {
|
|
74
|
+
lenis.raf(time);
|
|
75
|
+
requestAnimationFrame(raf);
|
|
76
|
+
}
|
|
77
|
+
requestAnimationFrame(raf);
|
|
78
|
+
|
|
79
|
+
return () => lenis.destroy();
|
|
80
|
+
}, []);
|
|
81
|
+
|
|
82
|
+
return <>{children}</>;
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Wrap `app/layout.tsx` body with `<LenisProvider>`.
|
|
87
|
+
|
|
88
|
+
**Getting original config:** In browser console on original site:
|
|
89
|
+
```javascript
|
|
90
|
+
window.lenis?.options // duration, easing, lerp, etc.
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## 3. GSAP + ScrollTrigger
|
|
96
|
+
|
|
97
|
+
**Detection:** `manifest.animations.libraries` includes `"gsap"`.
|
|
98
|
+
|
|
99
|
+
**Installation:**
|
|
100
|
+
```bash
|
|
101
|
+
npm install gsap
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**Extraction approach:**
|
|
105
|
+
In browser console on original site:
|
|
106
|
+
```javascript
|
|
107
|
+
// List all active tweens
|
|
108
|
+
gsap.globalTimeline.getChildren().forEach(t => {
|
|
109
|
+
console.log({
|
|
110
|
+
target: t.targets?.(),
|
|
111
|
+
duration: t.duration(),
|
|
112
|
+
vars: t.vars,
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// List all ScrollTrigger instances
|
|
117
|
+
ScrollTrigger.getAll().forEach(st => {
|
|
118
|
+
console.log({
|
|
119
|
+
trigger: st.trigger,
|
|
120
|
+
start: st.start,
|
|
121
|
+
end: st.end,
|
|
122
|
+
scrub: st.vars.scrub,
|
|
123
|
+
pin: st.vars.pin,
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**Recreation pattern:**
|
|
129
|
+
```typescript
|
|
130
|
+
'use client';
|
|
131
|
+
import { useEffect, useRef } from 'react';
|
|
132
|
+
import gsap from 'gsap';
|
|
133
|
+
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
|
134
|
+
|
|
135
|
+
gsap.registerPlugin(ScrollTrigger);
|
|
136
|
+
|
|
137
|
+
export function AnimatedSection() {
|
|
138
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
139
|
+
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
const ctx = gsap.context(() => {
|
|
142
|
+
gsap.from('.headline', {
|
|
143
|
+
y: 60,
|
|
144
|
+
opacity: 0,
|
|
145
|
+
duration: 0.8,
|
|
146
|
+
ease: 'power2.out',
|
|
147
|
+
scrollTrigger: {
|
|
148
|
+
trigger: ref.current,
|
|
149
|
+
start: 'top 80%',
|
|
150
|
+
toggleActions: 'play none none none',
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
}, ref);
|
|
154
|
+
|
|
155
|
+
return () => ctx.revert();
|
|
156
|
+
}, []);
|
|
157
|
+
|
|
158
|
+
return <div ref={ref}>...</div>;
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**Common GSAP patterns to look for:**
|
|
163
|
+
- `scrub: true` — animation tied to scroll position (not time-based)
|
|
164
|
+
- `pin: true` — section stays fixed while scroll animation plays
|
|
165
|
+
- `stagger` — sequential animation of multiple elements
|
|
166
|
+
- `timeline` — chained sequence of animations
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## 4. Framer Motion
|
|
171
|
+
|
|
172
|
+
**Detection:** `manifest.animations.libraries` includes `"framer-motion"`.
|
|
173
|
+
|
|
174
|
+
**Installation:**
|
|
175
|
+
```bash
|
|
176
|
+
npm install framer-motion
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**Extraction:** Framer Motion animations are React component props. In React DevTools,
|
|
180
|
+
inspect the component tree and look for `motion.*` elements with `initial`, `animate`,
|
|
181
|
+
`whileInView`, `variants` props.
|
|
182
|
+
|
|
183
|
+
If DevTools not available, infer from visual behavior:
|
|
184
|
+
- Fade in on scroll → `whileInView={{ opacity: 1 }}` with `initial={{ opacity: 0 }}`
|
|
185
|
+
- Slide up on scroll → add `y: 0` animate, `y: 40` initial
|
|
186
|
+
- Hover scale → `whileHover={{ scale: 1.05 }}`
|
|
187
|
+
- Exit animation → `exit={{ opacity: 0 }}`
|
|
188
|
+
|
|
189
|
+
**Recreation pattern:**
|
|
190
|
+
```typescript
|
|
191
|
+
import { motion } from 'framer-motion';
|
|
192
|
+
|
|
193
|
+
const fadeUp = {
|
|
194
|
+
hidden: { opacity: 0, y: 40 },
|
|
195
|
+
visible: { opacity: 1, y: 0, transition: { duration: 0.6, ease: 'easeOut' } },
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
export function FeatureCard() {
|
|
199
|
+
return (
|
|
200
|
+
<motion.div
|
|
201
|
+
variants={fadeUp}
|
|
202
|
+
initial="hidden"
|
|
203
|
+
whileInView="visible"
|
|
204
|
+
viewport={{ once: true, margin: '-100px' }}
|
|
205
|
+
>
|
|
206
|
+
...
|
|
207
|
+
</motion.div>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**Stagger children:**
|
|
213
|
+
```typescript
|
|
214
|
+
const container = {
|
|
215
|
+
hidden: {},
|
|
216
|
+
visible: { transition: { staggerChildren: 0.1 } },
|
|
217
|
+
};
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## 5. AOS (Animate on Scroll)
|
|
223
|
+
|
|
224
|
+
**Detection:** `manifest.animations.libraries` includes `"aos"` or elements have `data-aos` attributes.
|
|
225
|
+
|
|
226
|
+
**Extraction:**
|
|
227
|
+
```javascript
|
|
228
|
+
// In browser console — get all AOS attributes
|
|
229
|
+
document.querySelectorAll('[data-aos]').forEach(el => {
|
|
230
|
+
console.log({
|
|
231
|
+
selector: el.className,
|
|
232
|
+
aos: el.dataset.aos,
|
|
233
|
+
duration: el.dataset.aosDuration,
|
|
234
|
+
delay: el.dataset.aosDelay,
|
|
235
|
+
easing: el.dataset.aosEasing,
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
**Recreation options:**
|
|
241
|
+
|
|
242
|
+
Option A — Use AOS directly:
|
|
243
|
+
```bash
|
|
244
|
+
npm install aos
|
|
245
|
+
```
|
|
246
|
+
```typescript
|
|
247
|
+
'use client';
|
|
248
|
+
import { useEffect } from 'react';
|
|
249
|
+
import AOS from 'aos';
|
|
250
|
+
import 'aos/dist/aos.css';
|
|
251
|
+
|
|
252
|
+
export function AOSProvider({ children }) {
|
|
253
|
+
useEffect(() => { AOS.init({ duration: 800, once: true }); }, []);
|
|
254
|
+
return <>{children}</>;
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
Then add `data-aos="fade-up"` attributes to match original.
|
|
258
|
+
|
|
259
|
+
Option B — Replace with Framer Motion (cleaner, no extra dependency):
|
|
260
|
+
Map AOS animation names to Framer Motion variants.
|
|
261
|
+
`fade-up` → `{ hidden: { opacity: 0, y: 30 }, visible: { opacity: 1, y: 0 } }`
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## Animation Decision Tree
|
|
266
|
+
|
|
267
|
+
```
|
|
268
|
+
Is the animation triggered by scroll position?
|
|
269
|
+
→ YES: Is it scrubbed (tied to scroll position, not time)?
|
|
270
|
+
→ YES: GSAP with scrub: true
|
|
271
|
+
→ NO: GSAP ScrollTrigger OR Framer Motion whileInView OR IntersectionObserver
|
|
272
|
+
→ NO: Is it triggered on click?
|
|
273
|
+
→ YES: CSS transition OR Framer Motion AnimatePresence
|
|
274
|
+
→ NO: Is it on hover?
|
|
275
|
+
→ YES: CSS :hover transition OR Framer Motion whileHover
|
|
276
|
+
→ NO: Is it on page load?
|
|
277
|
+
→ YES: CSS animation OR Framer Motion initial/animate
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## Timing Extraction
|
|
283
|
+
|
|
284
|
+
If you can't get library config, estimate from visual inspection:
|
|
285
|
+
|
|
286
|
+
| Visual feel | Duration estimate | Easing |
|
|
287
|
+
|-------------|------------------|--------|
|
|
288
|
+
| Instant snap | 150-200ms | linear |
|
|
289
|
+
| Quick and clean | 200-300ms | ease-out |
|
|
290
|
+
| Smooth and polished | 300-500ms | cubic-bezier(0.4, 0, 0.2, 1) |
|
|
291
|
+
| Deliberate and dramatic | 600-900ms | power2.out (GSAP) |
|
|
292
|
+
| Scroll-scrubbed | no duration | scrub: true |
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# Behavior Spec Format — WebCloner Reference
|
|
2
|
+
|
|
3
|
+
YAML schema for describing interactive behaviors in component specs.
|
|
4
|
+
Paste into the `## States & Behaviors` section of each `docs/specs/*.spec.md` file.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Schema
|
|
9
|
+
|
|
10
|
+
```yaml
|
|
11
|
+
behaviors:
|
|
12
|
+
- name: string # human label, e.g. "Tab switch"
|
|
13
|
+
trigger:
|
|
14
|
+
type: click | hover | scroll | load | resize
|
|
15
|
+
selector: string # CSS selector of the interactive element
|
|
16
|
+
condition: string # optional — e.g. "scrollY > 80", "viewport < 768"
|
|
17
|
+
states:
|
|
18
|
+
[state-name]:
|
|
19
|
+
[property]: [value] # visual/content properties in this state
|
|
20
|
+
transition:
|
|
21
|
+
duration: string # e.g. "200ms"
|
|
22
|
+
easing: string # e.g. "ease-out", "cubic-bezier(0.4, 0, 0.2, 1)"
|
|
23
|
+
property: string # optional — which CSS property animates (all = default)
|
|
24
|
+
notes: string # optional — anything that doesn't fit above
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Examples by Behavior Type
|
|
30
|
+
|
|
31
|
+
### Click — Tab Switch
|
|
32
|
+
|
|
33
|
+
```yaml
|
|
34
|
+
behaviors:
|
|
35
|
+
- name: "Feature tabs"
|
|
36
|
+
trigger:
|
|
37
|
+
type: click
|
|
38
|
+
selector: ".tab-button"
|
|
39
|
+
states:
|
|
40
|
+
default:
|
|
41
|
+
activeIndex: 0
|
|
42
|
+
content: "First tab content visible"
|
|
43
|
+
activeTab:
|
|
44
|
+
background: "#000"
|
|
45
|
+
color: "#fff"
|
|
46
|
+
inactiveTab:
|
|
47
|
+
background: "transparent"
|
|
48
|
+
color: "#666"
|
|
49
|
+
tab-2:
|
|
50
|
+
activeIndex: 1
|
|
51
|
+
content: "Second tab content visible"
|
|
52
|
+
tab-3:
|
|
53
|
+
activeIndex: 2
|
|
54
|
+
content: "Third tab content visible"
|
|
55
|
+
transition:
|
|
56
|
+
duration: 200ms
|
|
57
|
+
easing: ease-out
|
|
58
|
+
property: opacity
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Scroll — Header Transform
|
|
62
|
+
|
|
63
|
+
```yaml
|
|
64
|
+
behaviors:
|
|
65
|
+
- name: "Sticky header"
|
|
66
|
+
trigger:
|
|
67
|
+
type: scroll
|
|
68
|
+
condition: "scrollY > 80"
|
|
69
|
+
states:
|
|
70
|
+
default:
|
|
71
|
+
position: fixed
|
|
72
|
+
top: 0
|
|
73
|
+
background: transparent
|
|
74
|
+
backdropFilter: none
|
|
75
|
+
padding: "24px 80px"
|
|
76
|
+
logo: large
|
|
77
|
+
scrolled:
|
|
78
|
+
background: "rgba(255,255,255,0.9)"
|
|
79
|
+
backdropFilter: "blur(12px)"
|
|
80
|
+
padding: "12px 80px"
|
|
81
|
+
boxShadow: "0 1px 0 rgba(0,0,0,0.08)"
|
|
82
|
+
logo: small
|
|
83
|
+
transition:
|
|
84
|
+
duration: 300ms
|
|
85
|
+
easing: "cubic-bezier(0.4, 0, 0.2, 1)"
|
|
86
|
+
property: all
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Hover — Card
|
|
90
|
+
|
|
91
|
+
```yaml
|
|
92
|
+
behaviors:
|
|
93
|
+
- name: "Feature card hover"
|
|
94
|
+
trigger:
|
|
95
|
+
type: hover
|
|
96
|
+
selector: ".feature-card"
|
|
97
|
+
states:
|
|
98
|
+
default:
|
|
99
|
+
transform: none
|
|
100
|
+
boxShadow: "0 2px 8px rgba(0,0,0,0.08)"
|
|
101
|
+
borderColor: "#e5e7eb"
|
|
102
|
+
hovered:
|
|
103
|
+
transform: "translateY(-4px)"
|
|
104
|
+
boxShadow: "0 12px 32px rgba(0,0,0,0.12)"
|
|
105
|
+
borderColor: "#000"
|
|
106
|
+
transition:
|
|
107
|
+
duration: 250ms
|
|
108
|
+
easing: ease-out
|
|
109
|
+
property: "transform, box-shadow, border-color"
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Load — Hero Entrance
|
|
113
|
+
|
|
114
|
+
```yaml
|
|
115
|
+
behaviors:
|
|
116
|
+
- name: "Hero entrance"
|
|
117
|
+
trigger:
|
|
118
|
+
type: load
|
|
119
|
+
condition: "DOMContentLoaded"
|
|
120
|
+
states:
|
|
121
|
+
before:
|
|
122
|
+
opacity: 0
|
|
123
|
+
transform: "translateY(30px)"
|
|
124
|
+
after:
|
|
125
|
+
opacity: 1
|
|
126
|
+
transform: "translateY(0)"
|
|
127
|
+
transition:
|
|
128
|
+
duration: 800ms
|
|
129
|
+
easing: "cubic-bezier(0.16, 1, 0.3, 1)"
|
|
130
|
+
notes: "Headline first (delay 0), subtitle +200ms, CTA +400ms (stagger)"
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Scroll-driven — Parallax
|
|
134
|
+
|
|
135
|
+
```yaml
|
|
136
|
+
behaviors:
|
|
137
|
+
- name: "Hero background parallax"
|
|
138
|
+
trigger:
|
|
139
|
+
type: scroll
|
|
140
|
+
condition: "element in viewport"
|
|
141
|
+
states:
|
|
142
|
+
top:
|
|
143
|
+
backgroundPositionY: "0%"
|
|
144
|
+
bottom:
|
|
145
|
+
backgroundPositionY: "30%"
|
|
146
|
+
transition:
|
|
147
|
+
duration: scrub # scrub = tied to scroll position, not time
|
|
148
|
+
easing: linear
|
|
149
|
+
notes: "GSAP scrub:true — background moves at 0.3x scroll speed"
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Click — Accordion
|
|
153
|
+
|
|
154
|
+
```yaml
|
|
155
|
+
behaviors:
|
|
156
|
+
- name: "FAQ accordion"
|
|
157
|
+
trigger:
|
|
158
|
+
type: click
|
|
159
|
+
selector: ".accordion-trigger"
|
|
160
|
+
states:
|
|
161
|
+
closed:
|
|
162
|
+
contentHeight: 0
|
|
163
|
+
overflow: hidden
|
|
164
|
+
icon: "+"
|
|
165
|
+
open:
|
|
166
|
+
contentHeight: auto # animate max-height from 0 to auto
|
|
167
|
+
overflow: visible
|
|
168
|
+
icon: "−"
|
|
169
|
+
transition:
|
|
170
|
+
duration: 300ms
|
|
171
|
+
easing: ease-out
|
|
172
|
+
property: max-height
|
|
173
|
+
notes: "Only one item open at a time. Opening one closes others."
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Hover — Navigation Dropdown
|
|
177
|
+
|
|
178
|
+
```yaml
|
|
179
|
+
behaviors:
|
|
180
|
+
- name: "Nav dropdown"
|
|
181
|
+
trigger:
|
|
182
|
+
type: hover
|
|
183
|
+
selector: ".nav-item[data-has-dropdown]"
|
|
184
|
+
states:
|
|
185
|
+
closed:
|
|
186
|
+
dropdownOpacity: 0
|
|
187
|
+
dropdownTransform: "translateY(-8px)"
|
|
188
|
+
dropdownPointerEvents: none
|
|
189
|
+
open:
|
|
190
|
+
dropdownOpacity: 1
|
|
191
|
+
dropdownTransform: "translateY(0)"
|
|
192
|
+
dropdownPointerEvents: auto
|
|
193
|
+
transition:
|
|
194
|
+
duration: 200ms
|
|
195
|
+
easing: ease-out
|
|
196
|
+
notes: "Close on mouseleave with 100ms delay to allow cursor travel to dropdown"
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Multi-state Components
|
|
202
|
+
|
|
203
|
+
When a component has more than 2 states, list all states explicitly:
|
|
204
|
+
|
|
205
|
+
```yaml
|
|
206
|
+
behaviors:
|
|
207
|
+
- name: "Carousel"
|
|
208
|
+
trigger:
|
|
209
|
+
type: click
|
|
210
|
+
selector: ".carousel-arrow"
|
|
211
|
+
states:
|
|
212
|
+
slide-1: { activeIndex: 0, transform: "translateX(0%)" }
|
|
213
|
+
slide-2: { activeIndex: 1, transform: "translateX(-100%)" }
|
|
214
|
+
slide-3: { activeIndex: 2, transform: "translateX(-200%)" }
|
|
215
|
+
transition:
|
|
216
|
+
duration: 400ms
|
|
217
|
+
easing: "cubic-bezier(0.4, 0, 0.2, 1)"
|
|
218
|
+
notes: "Loop: last → first. Dots sync with activeIndex."
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Scroll-triggered Entrance Animations
|
|
224
|
+
|
|
225
|
+
These use IntersectionObserver (or ScrollTrigger) — not pure scroll position.
|
|
226
|
+
|
|
227
|
+
```yaml
|
|
228
|
+
behaviors:
|
|
229
|
+
- name: "Section entrance"
|
|
230
|
+
trigger:
|
|
231
|
+
type: scroll
|
|
232
|
+
condition: "element enters viewport at threshold 0.2"
|
|
233
|
+
states:
|
|
234
|
+
before:
|
|
235
|
+
opacity: 0
|
|
236
|
+
transform: "translateY(40px)"
|
|
237
|
+
visible:
|
|
238
|
+
opacity: 1
|
|
239
|
+
transform: "translateY(0)"
|
|
240
|
+
transition:
|
|
241
|
+
duration: 600ms
|
|
242
|
+
easing: "cubic-bezier(0.16, 1, 0.3, 1)"
|
|
243
|
+
notes: "triggerOnce: true — does not reverse on scroll up"
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## Implementation Mapping
|
|
249
|
+
|
|
250
|
+
| Behavior type | Recommended implementation |
|
|
251
|
+
|---------------|---------------------------|
|
|
252
|
+
| CSS-only hover/focus | Tailwind `hover:` / `focus:` classes |
|
|
253
|
+
| Click toggle (React state) | `useState` + conditional className |
|
|
254
|
+
| Scroll position check | `useEffect` + scroll event listener |
|
|
255
|
+
| Scroll entrance animation | `react-intersection-observer` useInView |
|
|
256
|
+
| Scroll-scrubbed | GSAP + `scrub: true` |
|
|
257
|
+
| Smooth scroll feel | Lenis |
|
|
258
|
+
| Complex sequence/exit | Framer Motion `AnimatePresence` |
|
|
259
|
+
| Stagger children | Framer Motion `staggerChildren` or GSAP `stagger` |
|