tribunal-kit 2.4.5 → 3.0.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/.agent/agents/accessibility-reviewer.md +220 -134
- package/.agent/agents/ai-code-reviewer.md +233 -129
- package/.agent/agents/backend-specialist.md +238 -178
- package/.agent/agents/code-archaeologist.md +181 -119
- package/.agent/agents/database-architect.md +207 -164
- package/.agent/agents/debugger.md +218 -151
- package/.agent/agents/dependency-reviewer.md +136 -55
- package/.agent/agents/devops-engineer.md +238 -175
- package/.agent/agents/documentation-writer.md +221 -137
- package/.agent/agents/explorer-agent.md +180 -142
- package/.agent/agents/frontend-reviewer.md +194 -80
- package/.agent/agents/frontend-specialist.md +237 -188
- package/.agent/agents/game-developer.md +52 -184
- package/.agent/agents/logic-reviewer.md +149 -78
- package/.agent/agents/mobile-developer.md +223 -152
- package/.agent/agents/mobile-reviewer.md +195 -79
- package/.agent/agents/orchestrator.md +211 -170
- package/.agent/agents/penetration-tester.md +174 -131
- package/.agent/agents/performance-optimizer.md +203 -139
- package/.agent/agents/performance-reviewer.md +211 -108
- package/.agent/agents/product-manager.md +162 -108
- package/.agent/agents/project-planner.md +162 -142
- package/.agent/agents/qa-automation-engineer.md +242 -138
- package/.agent/agents/security-auditor.md +194 -170
- package/.agent/agents/seo-specialist.md +213 -132
- package/.agent/agents/sql-reviewer.md +194 -73
- package/.agent/agents/supervisor-agent.md +203 -156
- package/.agent/agents/test-coverage-reviewer.md +193 -81
- package/.agent/agents/type-safety-reviewer.md +208 -65
- package/.agent/scripts/__pycache__/auto_preview.cpython-311.pyc +0 -0
- package/.agent/scripts/__pycache__/bundle_analyzer.cpython-311.pyc +0 -0
- package/.agent/scripts/__pycache__/checklist.cpython-311.pyc +0 -0
- package/.agent/scripts/__pycache__/dependency_analyzer.cpython-311.pyc +0 -0
- package/.agent/scripts/__pycache__/security_scan.cpython-311.pyc +0 -0
- package/.agent/scripts/__pycache__/session_manager.cpython-311.pyc +0 -0
- package/.agent/scripts/__pycache__/skill_integrator.cpython-311.pyc +0 -0
- package/.agent/scripts/__pycache__/swarm_dispatcher.cpython-311.pyc +0 -0
- package/.agent/scripts/__pycache__/test_runner.cpython-311.pyc +0 -0
- package/.agent/scripts/__pycache__/verify_all.cpython-311.pyc +0 -0
- package/.agent/skills/agent-organizer/SKILL.md +126 -132
- package/.agent/skills/ai-prompt-injection-defense/SKILL.md +160 -0
- package/.agent/skills/api-patterns/SKILL.md +289 -257
- package/.agent/skills/api-security-auditor/SKILL.md +177 -0
- package/.agent/skills/app-builder/templates/chrome-extension/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/electron-desktop/TEMPLATE.md +1 -1
- package/.agent/skills/appflow-wireframe/SKILL.md +107 -58
- package/.agent/skills/architecture/SKILL.md +331 -200
- package/.agent/skills/authentication-best-practices/SKILL.md +173 -0
- package/.agent/skills/bash-linux/SKILL.md +154 -215
- package/.agent/skills/brainstorming/SKILL.md +104 -210
- package/.agent/skills/building-native-ui/SKILL.md +174 -0
- package/.agent/skills/clean-code/SKILL.md +360 -206
- package/.agent/skills/config-validator/SKILL.md +141 -165
- package/.agent/skills/csharp-developer/SKILL.md +528 -107
- package/.agent/skills/database-design/SKILL.md +455 -275
- package/.agent/skills/deployment-procedures/SKILL.md +145 -188
- package/.agent/skills/devops-engineer/SKILL.md +332 -134
- package/.agent/skills/devops-incident-responder/SKILL.md +113 -98
- package/.agent/skills/edge-computing/SKILL.md +157 -213
- package/.agent/skills/extract-design-system/SKILL.md +134 -0
- package/.agent/skills/framer-motion-expert/SKILL.md +939 -0
- package/.agent/skills/game-design-expert/SKILL.md +105 -0
- package/.agent/skills/game-engineering-expert/SKILL.md +122 -0
- package/.agent/skills/geo-fundamentals/SKILL.md +124 -215
- package/.agent/skills/github-operations/SKILL.md +314 -354
- package/.agent/skills/gsap-expert/SKILL.md +901 -0
- package/.agent/skills/i18n-localization/SKILL.md +138 -216
- package/.agent/skills/intelligent-routing/SKILL.md +127 -139
- package/.agent/skills/llm-engineering/SKILL.md +357 -258
- package/.agent/skills/local-first/SKILL.md +154 -203
- package/.agent/skills/mcp-builder/SKILL.md +118 -224
- package/.agent/skills/nextjs-react-expert/SKILL.md +783 -203
- package/.agent/skills/nodejs-best-practices/SKILL.md +559 -280
- package/.agent/skills/observability/SKILL.md +330 -285
- package/.agent/skills/parallel-agents/SKILL.md +122 -181
- package/.agent/skills/performance-profiling/SKILL.md +254 -197
- package/.agent/skills/plan-writing/SKILL.md +118 -188
- package/.agent/skills/platform-engineer/SKILL.md +123 -135
- package/.agent/skills/playwright-best-practices/SKILL.md +162 -0
- package/.agent/skills/powershell-windows/SKILL.md +146 -230
- package/.agent/skills/python-pro/SKILL.md +879 -114
- package/.agent/skills/react-specialist/SKILL.md +931 -108
- package/.agent/skills/readme-builder/SKILL.md +42 -0
- package/.agent/skills/realtime-patterns/SKILL.md +304 -296
- package/.agent/skills/rust-pro/SKILL.md +701 -240
- package/.agent/skills/seo-fundamentals/SKILL.md +154 -181
- package/.agent/skills/server-management/SKILL.md +190 -212
- package/.agent/skills/shadcn-ui-expert/SKILL.md +206 -0
- package/.agent/skills/skill-creator/SKILL.md +68 -0
- package/.agent/skills/sql-pro/SKILL.md +633 -104
- package/.agent/skills/supabase-postgres-best-practices/SKILL.md +78 -0
- package/.agent/skills/swiftui-expert/SKILL.md +176 -0
- package/.agent/skills/systematic-debugging/SKILL.md +118 -186
- package/.agent/skills/tailwind-patterns/SKILL.md +576 -232
- package/.agent/skills/tdd-workflow/SKILL.md +137 -209
- package/.agent/skills/testing-patterns/SKILL.md +573 -205
- package/.agent/skills/vue-expert/SKILL.md +964 -119
- package/.agent/skills/vulnerability-scanner/SKILL.md +269 -316
- package/.agent/skills/web-accessibility-auditor/SKILL.md +193 -0
- package/.agent/skills/webapp-testing/SKILL.md +145 -236
- package/.agent/workflows/api-tester.md +151 -279
- package/.agent/workflows/audit.md +138 -168
- package/.agent/workflows/brainstorm.md +110 -146
- package/.agent/workflows/changelog.md +112 -144
- package/.agent/workflows/create.md +124 -139
- package/.agent/workflows/debug.md +189 -196
- package/.agent/workflows/deploy.md +189 -153
- package/.agent/workflows/enhance.md +151 -139
- package/.agent/workflows/fix.md +135 -143
- package/.agent/workflows/generate.md +157 -164
- package/.agent/workflows/migrate.md +160 -163
- package/.agent/workflows/orchestrate.md +168 -151
- package/.agent/workflows/performance-benchmarker.md +123 -305
- package/.agent/workflows/plan.md +173 -151
- package/.agent/workflows/preview.md +80 -137
- package/.agent/workflows/refactor.md +183 -153
- package/.agent/workflows/review-ai.md +129 -140
- package/.agent/workflows/review.md +116 -155
- package/.agent/workflows/session.md +94 -154
- package/.agent/workflows/status.md +79 -125
- package/.agent/workflows/strengthen-skills.md +139 -99
- package/.agent/workflows/swarm.md +179 -194
- package/.agent/workflows/test.md +211 -166
- package/.agent/workflows/tribunal-backend.md +113 -111
- package/.agent/workflows/tribunal-database.md +115 -132
- package/.agent/workflows/tribunal-frontend.md +118 -115
- package/.agent/workflows/tribunal-full.md +133 -136
- package/.agent/workflows/tribunal-mobile.md +119 -123
- package/.agent/workflows/tribunal-performance.md +133 -152
- package/.agent/workflows/ui-ux-pro-max.md +143 -171
- package/README.md +11 -15
- package/package.json +1 -1
- package/.agent/skills/dotnet-core-expert/SKILL.md +0 -103
- package/.agent/skills/game-development/2d-games/SKILL.md +0 -119
- package/.agent/skills/game-development/3d-games/SKILL.md +0 -135
- package/.agent/skills/game-development/SKILL.md +0 -236
- package/.agent/skills/game-development/game-art/SKILL.md +0 -185
- package/.agent/skills/game-development/game-audio/SKILL.md +0 -190
- package/.agent/skills/game-development/game-design/SKILL.md +0 -129
- package/.agent/skills/game-development/mobile-games/SKILL.md +0 -108
- package/.agent/skills/game-development/multiplayer/SKILL.md +0 -132
- package/.agent/skills/game-development/pc-games/SKILL.md +0 -144
- package/.agent/skills/game-development/vr-ar/SKILL.md +0 -123
- package/.agent/skills/game-development/web-games/SKILL.md +0 -150
|
@@ -0,0 +1,939 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: framer-motion-expert
|
|
3
|
+
description: Framer Motion 12+ mastery for React. Declarative animations, layout transitions, gestures, scroll-linked motion, AnimatePresence, useAnimate, useMotionValue, LazyMotion bundle optimization, and accessibility (prefers-reduced-motion). Use when building React component animations, page transitions, shared layout animations, or gesture-driven UI.
|
|
4
|
+
allowed-tools: Read, Write, Edit, Glob, Grep
|
|
5
|
+
version: 1.0.0
|
|
6
|
+
last-updated: 2026-03-30
|
|
7
|
+
applies-to-model: gemini-2.5-pro, claude-3-7-sonnet
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Framer Motion Expert — React Animation Library
|
|
11
|
+
|
|
12
|
+
> Framer Motion is declarative-first. If you're writing imperative animation loops in React, you're fighting the framework.
|
|
13
|
+
> Every `motion.div` must have a reason. Every `AnimatePresence` must have a `key`. Every `useMotionValue` must avoid re-renders.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Core API — Declarative Animations
|
|
18
|
+
|
|
19
|
+
### The `motion` Component
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
import { motion } from "framer-motion";
|
|
23
|
+
|
|
24
|
+
// motion.div, motion.span, motion.button, motion.svg, motion.path, etc.
|
|
25
|
+
// Any HTML or SVG element can be prefixed with `motion.`
|
|
26
|
+
|
|
27
|
+
function FadeInBox() {
|
|
28
|
+
return (
|
|
29
|
+
<motion.div
|
|
30
|
+
initial={{ opacity: 0, y: 20 }}
|
|
31
|
+
animate={{ opacity: 1, y: 0 }}
|
|
32
|
+
transition={{ duration: 0.5, ease: "easeOut" }}
|
|
33
|
+
>
|
|
34
|
+
Hello, animated world
|
|
35
|
+
</motion.div>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ❌ HALLUCINATION TRAP: There is NO <Motion> component (capital M)
|
|
40
|
+
// ❌ HALLUCINATION TRAP: There is NO motion() function wrapper
|
|
41
|
+
// ✅ It's always motion.div, motion.span, etc. (lowercase dot notation)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### The `animate` Prop
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
// Object syntax (most common)
|
|
48
|
+
<motion.div animate={{ x: 100, opacity: 1 }} />
|
|
49
|
+
|
|
50
|
+
// Dynamic — responds to state changes automatically
|
|
51
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
52
|
+
<motion.div animate={{ height: isOpen ? "auto" : 0 }} />
|
|
53
|
+
|
|
54
|
+
// ❌ HALLUCINATION TRAP: `animate={{ height: "auto" }}` only works
|
|
55
|
+
// in Framer Motion 11+ with the layout animation engine.
|
|
56
|
+
// For older versions, use explicit pixel values or the `layout` prop.
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### `initial`, `animate`, `exit`
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
<AnimatePresence>
|
|
63
|
+
{isVisible && (
|
|
64
|
+
<motion.div
|
|
65
|
+
key="modal" // ← key is REQUIRED inside AnimatePresence
|
|
66
|
+
initial={{ opacity: 0, scale: 0.95 }}
|
|
67
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
68
|
+
exit={{ opacity: 0, scale: 0.95 }}
|
|
69
|
+
transition={{ duration: 0.3 }}
|
|
70
|
+
>
|
|
71
|
+
Modal content
|
|
72
|
+
</motion.div>
|
|
73
|
+
)}
|
|
74
|
+
</AnimatePresence>
|
|
75
|
+
|
|
76
|
+
// ❌ HALLUCINATION TRAP: exit animations do NOT work without AnimatePresence
|
|
77
|
+
// ❌ HALLUCINATION TRAP: AnimatePresence children MUST have a unique `key`
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Variants (Declarative Animation Maps)
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
const containerVariants = {
|
|
84
|
+
hidden: { opacity: 0 },
|
|
85
|
+
visible: {
|
|
86
|
+
opacity: 1,
|
|
87
|
+
transition: {
|
|
88
|
+
staggerChildren: 0.1, // stagger between children
|
|
89
|
+
delayChildren: 0.2, // delay before first child
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const itemVariants = {
|
|
95
|
+
hidden: { opacity: 0, y: 20 },
|
|
96
|
+
visible: { opacity: 1, y: 0 },
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
function List() {
|
|
100
|
+
return (
|
|
101
|
+
<motion.ul
|
|
102
|
+
variants={containerVariants}
|
|
103
|
+
initial="hidden"
|
|
104
|
+
animate="visible"
|
|
105
|
+
>
|
|
106
|
+
{items.map((item) => (
|
|
107
|
+
<motion.li key={item.id} variants={itemVariants}>
|
|
108
|
+
{item.name}
|
|
109
|
+
</motion.li>
|
|
110
|
+
))}
|
|
111
|
+
</motion.ul>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Variant names propagate automatically to children
|
|
116
|
+
// Children inherit the current variant name from parent
|
|
117
|
+
// You do NOT need to set initial/animate on each child
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Transitions
|
|
123
|
+
|
|
124
|
+
### Tween (Default)
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
<motion.div
|
|
128
|
+
animate={{ x: 100 }}
|
|
129
|
+
transition={{
|
|
130
|
+
type: "tween", // default for most properties
|
|
131
|
+
duration: 0.5,
|
|
132
|
+
ease: "easeInOut", // or [0.42, 0, 0.58, 1] (cubic-bezier)
|
|
133
|
+
delay: 0.2,
|
|
134
|
+
repeat: 2, // number of repeats (Infinity for loop)
|
|
135
|
+
repeatType: "reverse", // "loop", "reverse", "mirror"
|
|
136
|
+
repeatDelay: 0.5,
|
|
137
|
+
}}
|
|
138
|
+
/>
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Spring (Physics-Based)
|
|
142
|
+
|
|
143
|
+
```tsx
|
|
144
|
+
<motion.div
|
|
145
|
+
animate={{ x: 100 }}
|
|
146
|
+
transition={{
|
|
147
|
+
type: "spring",
|
|
148
|
+
stiffness: 300, // spring tension (default: 100)
|
|
149
|
+
damping: 20, // resistance (default: 10)
|
|
150
|
+
mass: 1, // weight (default: 1)
|
|
151
|
+
bounce: 0.25, // shorthand: 0 = no bounce, 1 = max bounce
|
|
152
|
+
// duration + bounce is an alternative to stiffness + damping
|
|
153
|
+
duration: 0.8, // approximate duration (auto-calculates stiffness/damping)
|
|
154
|
+
}}
|
|
155
|
+
/>
|
|
156
|
+
|
|
157
|
+
// ❌ HALLUCINATION TRAP: You cannot use BOTH stiffness+damping AND duration+bounce
|
|
158
|
+
// Pick one pair. Using both causes unpredictable behavior.
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Inertia
|
|
162
|
+
|
|
163
|
+
```tsx
|
|
164
|
+
// Inertia transitions are used after drag gestures
|
|
165
|
+
<motion.div
|
|
166
|
+
drag="x"
|
|
167
|
+
dragTransition={{
|
|
168
|
+
bounceStiffness: 600,
|
|
169
|
+
bounceDamping: 20,
|
|
170
|
+
power: 0.8, // deceleration rate
|
|
171
|
+
timeConstant: 750, // ms to reach ~63% of projected distance
|
|
172
|
+
}}
|
|
173
|
+
/>
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Orchestration
|
|
177
|
+
|
|
178
|
+
```tsx
|
|
179
|
+
// Parent controls when children animate
|
|
180
|
+
const parent = {
|
|
181
|
+
visible: {
|
|
182
|
+
transition: {
|
|
183
|
+
when: "beforeChildren", // "afterChildren", "beforeChildren"
|
|
184
|
+
staggerChildren: 0.1,
|
|
185
|
+
staggerDirection: 1, // 1 = forward, -1 = reverse
|
|
186
|
+
delayChildren: 0.3,
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Per-Property Transitions
|
|
193
|
+
|
|
194
|
+
```tsx
|
|
195
|
+
<motion.div
|
|
196
|
+
animate={{ x: 100, opacity: 1 }}
|
|
197
|
+
transition={{
|
|
198
|
+
x: { type: "spring", stiffness: 300 },
|
|
199
|
+
opacity: { duration: 0.2 },
|
|
200
|
+
default: { duration: 0.5 }, // fallback for unlisted properties
|
|
201
|
+
}}
|
|
202
|
+
/>
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Gestures
|
|
208
|
+
|
|
209
|
+
### Hover, Tap, Focus
|
|
210
|
+
|
|
211
|
+
```tsx
|
|
212
|
+
<motion.button
|
|
213
|
+
whileHover={{ scale: 1.05, backgroundColor: "#4338ca" }}
|
|
214
|
+
whileTap={{ scale: 0.95 }}
|
|
215
|
+
whileFocus={{ boxShadow: "0 0 0 3px rgba(66, 153, 225, 0.6)" }}
|
|
216
|
+
transition={{ type: "spring", stiffness: 400, damping: 15 }}
|
|
217
|
+
>
|
|
218
|
+
Click me
|
|
219
|
+
</motion.button>
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Drag
|
|
223
|
+
|
|
224
|
+
```tsx
|
|
225
|
+
<motion.div
|
|
226
|
+
drag // "x", "y", or true for both axes
|
|
227
|
+
dragConstraints={{ left: -100, right: 100, top: -50, bottom: 50 }}
|
|
228
|
+
dragElastic={0.2} // 0 = hard stop, 1 = free (default: 0.35)
|
|
229
|
+
dragMomentum={true} // continue with inertia after release (default: true)
|
|
230
|
+
dragSnapToOrigin // return to starting position on release
|
|
231
|
+
onDragStart={(event, info) => console.log(info.point)}
|
|
232
|
+
onDrag={(event, info) => console.log(info.offset)}
|
|
233
|
+
onDragEnd={(event, info) => console.log(info.velocity)}
|
|
234
|
+
/>
|
|
235
|
+
|
|
236
|
+
// Drag within a parent container
|
|
237
|
+
function DragInContainer() {
|
|
238
|
+
const constraintsRef = useRef(null);
|
|
239
|
+
return (
|
|
240
|
+
<motion.div ref={constraintsRef} style={{ width: 400, height: 400 }}>
|
|
241
|
+
<motion.div drag dragConstraints={constraintsRef} />
|
|
242
|
+
</motion.div>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### `whileInView` (Scroll-Triggered)
|
|
248
|
+
|
|
249
|
+
```tsx
|
|
250
|
+
<motion.div
|
|
251
|
+
initial={{ opacity: 0, y: 50 }}
|
|
252
|
+
whileInView={{ opacity: 1, y: 0 }}
|
|
253
|
+
viewport={{
|
|
254
|
+
once: true, // only animate once (do NOT re-trigger on scroll back)
|
|
255
|
+
amount: 0.3, // 30% of element must be visible
|
|
256
|
+
margin: "-100px", // shrink viewport detection area
|
|
257
|
+
}}
|
|
258
|
+
transition={{ duration: 0.6 }}
|
|
259
|
+
>
|
|
260
|
+
Appears on scroll
|
|
261
|
+
</motion.div>
|
|
262
|
+
|
|
263
|
+
// ❌ HALLUCINATION TRAP: `viewport.once` defaults to false
|
|
264
|
+
// This means the animation replays every time the element enters/exits
|
|
265
|
+
// Most designs want `once: true` for entrance animations
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## Layout Animations
|
|
271
|
+
|
|
272
|
+
### The `layout` Prop
|
|
273
|
+
|
|
274
|
+
```tsx
|
|
275
|
+
// The MAGIC of Framer Motion — automatic layout animations
|
|
276
|
+
// When a component's size or position changes,
|
|
277
|
+
// Framer Motion animates between the old and new layout automatically
|
|
278
|
+
|
|
279
|
+
function ExpandableCard({ isExpanded }) {
|
|
280
|
+
return (
|
|
281
|
+
<motion.div
|
|
282
|
+
layout // ← THIS is the magic prop
|
|
283
|
+
style={{
|
|
284
|
+
width: isExpanded ? 400 : 200,
|
|
285
|
+
height: isExpanded ? 300 : 100,
|
|
286
|
+
}}
|
|
287
|
+
transition={{ type: "spring", stiffness: 200, damping: 25 }}
|
|
288
|
+
>
|
|
289
|
+
<motion.p layout="position">
|
|
290
|
+
{/* layout="position" — only animate position, not size */}
|
|
291
|
+
Card content
|
|
292
|
+
</motion.p>
|
|
293
|
+
</motion.div>
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// layout values:
|
|
298
|
+
// true — animate both position and size
|
|
299
|
+
// "position" — only animate position (prevents text reflow flicker)
|
|
300
|
+
// "size" — only animate size
|
|
301
|
+
// "preserve-aspect" — maintain aspect ratio during transition
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### `layoutId` — Shared Element Transitions
|
|
305
|
+
|
|
306
|
+
```tsx
|
|
307
|
+
// The most powerful feature in Framer Motion
|
|
308
|
+
// Elements with the same layoutId across renders morph into each other
|
|
309
|
+
|
|
310
|
+
function ItemList({ selectedId, onSelect }) {
|
|
311
|
+
return (
|
|
312
|
+
<div className="grid">
|
|
313
|
+
{items.map((item) => (
|
|
314
|
+
<motion.div
|
|
315
|
+
key={item.id}
|
|
316
|
+
layoutId={`card-${item.id}`}
|
|
317
|
+
onClick={() => onSelect(item.id)}
|
|
318
|
+
>
|
|
319
|
+
<motion.h2 layoutId={`title-${item.id}`}>{item.title}</motion.h2>
|
|
320
|
+
</motion.div>
|
|
321
|
+
))}
|
|
322
|
+
|
|
323
|
+
<AnimatePresence>
|
|
324
|
+
{selectedId && (
|
|
325
|
+
<motion.div
|
|
326
|
+
layoutId={`card-${selectedId}`}
|
|
327
|
+
className="expanded-card"
|
|
328
|
+
>
|
|
329
|
+
<motion.h2 layoutId={`title-${selectedId}`}>
|
|
330
|
+
{items.find(i => i.id === selectedId).title}
|
|
331
|
+
</motion.h2>
|
|
332
|
+
<motion.p
|
|
333
|
+
initial={{ opacity: 0 }}
|
|
334
|
+
animate={{ opacity: 1 }}
|
|
335
|
+
exit={{ opacity: 0 }}
|
|
336
|
+
>
|
|
337
|
+
Expanded content...
|
|
338
|
+
</motion.p>
|
|
339
|
+
</motion.div>
|
|
340
|
+
)}
|
|
341
|
+
</AnimatePresence>
|
|
342
|
+
</div>
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ❌ HALLUCINATION TRAP: layoutId elements MUST be in the same LayoutGroup
|
|
347
|
+
// or be siblings under the same AnimatePresence. Cross-tree layoutId
|
|
348
|
+
// requires wrapping in <LayoutGroup>.
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### `LayoutGroup`
|
|
352
|
+
|
|
353
|
+
```tsx
|
|
354
|
+
import { LayoutGroup } from "framer-motion";
|
|
355
|
+
|
|
356
|
+
// Required when layoutId elements span different component trees
|
|
357
|
+
<LayoutGroup>
|
|
358
|
+
<Sidebar />
|
|
359
|
+
<MainContent />
|
|
360
|
+
</LayoutGroup>
|
|
361
|
+
|
|
362
|
+
// Also useful to prevent layout animations from affecting siblings
|
|
363
|
+
<LayoutGroup id="sidebar">
|
|
364
|
+
{/* Layout animations here won't leak to the rest of the page */}
|
|
365
|
+
</LayoutGroup>
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
## Scroll Animations
|
|
371
|
+
|
|
372
|
+
### `useScroll`
|
|
373
|
+
|
|
374
|
+
```tsx
|
|
375
|
+
import { motion, useScroll, useTransform } from "framer-motion";
|
|
376
|
+
|
|
377
|
+
function ParallaxHero() {
|
|
378
|
+
const { scrollYProgress } = useScroll();
|
|
379
|
+
|
|
380
|
+
// Map scroll progress (0–1) to a y offset (-50 to 50)
|
|
381
|
+
const y = useTransform(scrollYProgress, [0, 1], [0, -200]);
|
|
382
|
+
const opacity = useTransform(scrollYProgress, [0, 0.5], [1, 0]);
|
|
383
|
+
|
|
384
|
+
return (
|
|
385
|
+
<motion.div style={{ y, opacity }}>
|
|
386
|
+
<h1>Parallax Hero</h1>
|
|
387
|
+
</motion.div>
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
### Element-Scoped Scroll Tracking
|
|
393
|
+
|
|
394
|
+
```tsx
|
|
395
|
+
function ProgressBar() {
|
|
396
|
+
const ref = useRef(null);
|
|
397
|
+
const { scrollYProgress } = useScroll({
|
|
398
|
+
target: ref, // track this element's scroll position
|
|
399
|
+
offset: ["start end", "end start"], // [trigger start, trigger end]
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
return (
|
|
403
|
+
<div ref={ref}>
|
|
404
|
+
<motion.div
|
|
405
|
+
style={{ scaleX: scrollYProgress, transformOrigin: "left" }}
|
|
406
|
+
className="progress-bar"
|
|
407
|
+
/>
|
|
408
|
+
Long content...
|
|
409
|
+
</div>
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
### `useTransform` Chains
|
|
415
|
+
|
|
416
|
+
```tsx
|
|
417
|
+
const { scrollYProgress } = useScroll();
|
|
418
|
+
|
|
419
|
+
// Chain multiple transforms
|
|
420
|
+
const scale = useTransform(scrollYProgress, [0, 0.5, 1], [1, 1.5, 1]);
|
|
421
|
+
const rotate = useTransform(scrollYProgress, [0, 1], [0, 360]);
|
|
422
|
+
const color = useTransform(
|
|
423
|
+
scrollYProgress,
|
|
424
|
+
[0, 0.5, 1],
|
|
425
|
+
["#ff0000", "#00ff00", "#0000ff"]
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
// Derived transforms
|
|
429
|
+
const invertedY = useTransform(scrollYProgress, (v) => 1 - v);
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
---
|
|
433
|
+
|
|
434
|
+
## Hooks
|
|
435
|
+
|
|
436
|
+
### `useAnimate` (Imperative Control)
|
|
437
|
+
|
|
438
|
+
```tsx
|
|
439
|
+
import { useAnimate, stagger } from "framer-motion";
|
|
440
|
+
|
|
441
|
+
function AnimatedList() {
|
|
442
|
+
const [scope, animate] = useAnimate();
|
|
443
|
+
|
|
444
|
+
async function handleClick() {
|
|
445
|
+
// Imperative sequence
|
|
446
|
+
await animate(".list-item", { opacity: 1, y: 0 }, {
|
|
447
|
+
delay: stagger(0.1),
|
|
448
|
+
duration: 0.3,
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
await animate(".cta-button", { scale: [1, 1.1, 1] }, {
|
|
452
|
+
duration: 0.4,
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return (
|
|
457
|
+
<div ref={scope}>
|
|
458
|
+
{items.map((item) => (
|
|
459
|
+
<div key={item.id} className="list-item" style={{ opacity: 0 }}>
|
|
460
|
+
{item.name}
|
|
461
|
+
</div>
|
|
462
|
+
))}
|
|
463
|
+
<button className="cta-button" onClick={handleClick}>
|
|
464
|
+
Show All
|
|
465
|
+
</button>
|
|
466
|
+
</div>
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// ❌ HALLUCINATION TRAP: useAnimate returns [scope, animate]
|
|
471
|
+
// NOT [ref, controls] — that was the old useCycle/useAnimationControls API
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
### `useMotionValue`
|
|
475
|
+
|
|
476
|
+
```tsx
|
|
477
|
+
import { motion, useMotionValue, useTransform } from "framer-motion";
|
|
478
|
+
|
|
479
|
+
function RotatingCard() {
|
|
480
|
+
const x = useMotionValue(0);
|
|
481
|
+
const rotateY = useTransform(x, [-200, 200], [-45, 45]);
|
|
482
|
+
const background = useTransform(
|
|
483
|
+
x,
|
|
484
|
+
[-200, 0, 200],
|
|
485
|
+
["#ff008c", "#7700ff", "#00d4ff"]
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
return (
|
|
489
|
+
<motion.div
|
|
490
|
+
style={{ x, rotateY, background }}
|
|
491
|
+
drag="x"
|
|
492
|
+
dragConstraints={{ left: -200, right: 200 }}
|
|
493
|
+
/>
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// ✅ useMotionValue does NOT trigger React re-renders
|
|
498
|
+
// This is the key performance advantage over useState for animations
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
### `useSpring`
|
|
502
|
+
|
|
503
|
+
```tsx
|
|
504
|
+
import { useSpring, useMotionValue } from "framer-motion";
|
|
505
|
+
|
|
506
|
+
const x = useMotionValue(0);
|
|
507
|
+
const springX = useSpring(x, {
|
|
508
|
+
stiffness: 300,
|
|
509
|
+
damping: 30,
|
|
510
|
+
restDelta: 0.001, // stop spring when movement is below this
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
// springX automatically follows x with spring physics
|
|
514
|
+
// Use springX in style={{ x: springX }} for smooth following
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
### `useVelocity`
|
|
518
|
+
|
|
519
|
+
```tsx
|
|
520
|
+
import { useMotionValue, useVelocity, useTransform } from "framer-motion";
|
|
521
|
+
|
|
522
|
+
const x = useMotionValue(0);
|
|
523
|
+
const xVelocity = useVelocity(x);
|
|
524
|
+
const skewX = useTransform(xVelocity, [-1000, 0, 1000], [-15, 0, 15]);
|
|
525
|
+
|
|
526
|
+
// Skews element based on drag speed — creates a "rubber" feel
|
|
527
|
+
<motion.div style={{ x, skewX }} drag="x" />
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
---
|
|
531
|
+
|
|
532
|
+
## AnimatePresence (Mount/Unmount Animations)
|
|
533
|
+
|
|
534
|
+
```tsx
|
|
535
|
+
import { AnimatePresence, motion } from "framer-motion";
|
|
536
|
+
|
|
537
|
+
function Notifications({ items }) {
|
|
538
|
+
return (
|
|
539
|
+
<AnimatePresence
|
|
540
|
+
mode="sync" // "sync" | "wait" | "popLayout"
|
|
541
|
+
initial={false} // skip initial animation on first render
|
|
542
|
+
>
|
|
543
|
+
{items.map((item) => (
|
|
544
|
+
<motion.div
|
|
545
|
+
key={item.id} // ← UNIQUE KEY IS MANDATORY
|
|
546
|
+
initial={{ opacity: 0, height: 0 }}
|
|
547
|
+
animate={{ opacity: 1, height: "auto" }}
|
|
548
|
+
exit={{ opacity: 0, height: 0 }}
|
|
549
|
+
transition={{ duration: 0.3 }}
|
|
550
|
+
>
|
|
551
|
+
{item.message}
|
|
552
|
+
</motion.div>
|
|
553
|
+
))}
|
|
554
|
+
</AnimatePresence>
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Modes:
|
|
559
|
+
// "sync" — new and old animate simultaneously (default)
|
|
560
|
+
// "wait" — wait for exit to finish before entering
|
|
561
|
+
// "popLayout" — uses FLIP to handle layout shifts during exit
|
|
562
|
+
|
|
563
|
+
// ❌ HALLUCINATION TRAP: `mode="wait"` used to be called `exitBeforeEnter`
|
|
564
|
+
// exitBeforeEnter was REMOVED in Framer Motion 7+
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
### Page Transitions (Next.js App Router)
|
|
568
|
+
|
|
569
|
+
```tsx
|
|
570
|
+
// layout.tsx
|
|
571
|
+
"use client";
|
|
572
|
+
import { AnimatePresence } from "framer-motion";
|
|
573
|
+
|
|
574
|
+
export default function Layout({ children }: { children: React.ReactNode }) {
|
|
575
|
+
return (
|
|
576
|
+
<AnimatePresence mode="wait">
|
|
577
|
+
{children}
|
|
578
|
+
</AnimatePresence>
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// page.tsx
|
|
583
|
+
"use client";
|
|
584
|
+
import { motion } from "framer-motion";
|
|
585
|
+
|
|
586
|
+
export default function Page() {
|
|
587
|
+
return (
|
|
588
|
+
<motion.main
|
|
589
|
+
initial={{ opacity: 0, y: 20 }}
|
|
590
|
+
animate={{ opacity: 1, y: 0 }}
|
|
591
|
+
exit={{ opacity: 0, y: -20 }}
|
|
592
|
+
transition={{ duration: 0.3 }}
|
|
593
|
+
>
|
|
594
|
+
Page content
|
|
595
|
+
</motion.main>
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
---
|
|
601
|
+
|
|
602
|
+
## Performance & Accessibility
|
|
603
|
+
|
|
604
|
+
### `m` vs `motion` — Bundle Optimization (LazyMotion)
|
|
605
|
+
|
|
606
|
+
```tsx
|
|
607
|
+
import { LazyMotion, domAnimation, m } from "framer-motion";
|
|
608
|
+
|
|
609
|
+
// LazyMotion + domAnimation = ~5KB instead of ~30KB
|
|
610
|
+
// Use `m.div` instead of `motion.div` inside LazyMotion
|
|
611
|
+
|
|
612
|
+
function App() {
|
|
613
|
+
return (
|
|
614
|
+
<LazyMotion features={domAnimation}>
|
|
615
|
+
<m.div
|
|
616
|
+
initial={{ opacity: 0 }}
|
|
617
|
+
animate={{ opacity: 1 }}
|
|
618
|
+
>
|
|
619
|
+
Lightweight animation
|
|
620
|
+
</m.div>
|
|
621
|
+
</LazyMotion>
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// For full feature set (layout animations, drag, etc.):
|
|
626
|
+
import { domMax } from "framer-motion";
|
|
627
|
+
<LazyMotion features={domMax}>
|
|
628
|
+
|
|
629
|
+
// ❌ HALLUCINATION TRAP: m.div does NOT work without LazyMotion wrapper
|
|
630
|
+
// ❌ HALLUCINATION TRAP: layout animations require domMax, not domAnimation
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
### `prefers-reduced-motion` (Accessibility — Mandatory)
|
|
634
|
+
|
|
635
|
+
```tsx
|
|
636
|
+
import { useReducedMotion } from "framer-motion";
|
|
637
|
+
|
|
638
|
+
function AnimatedComponent() {
|
|
639
|
+
const shouldReduceMotion = useReducedMotion();
|
|
640
|
+
|
|
641
|
+
return (
|
|
642
|
+
<motion.div
|
|
643
|
+
animate={{
|
|
644
|
+
x: shouldReduceMotion ? 0 : 100,
|
|
645
|
+
opacity: 1, // opacity changes are always safe
|
|
646
|
+
}}
|
|
647
|
+
transition={{
|
|
648
|
+
duration: shouldReduceMotion ? 0 : 0.5,
|
|
649
|
+
}}
|
|
650
|
+
>
|
|
651
|
+
Accessible animation
|
|
652
|
+
</motion.div>
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// ✅ RULE: Opacity and color transitions are acceptable for reduced-motion users
|
|
657
|
+
// ❌ RULE: Position, scale, and rotation animations must be disabled or minimized
|
|
658
|
+
// ❌ RULE: Auto-playing looping animations MUST stop under reduced motion
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
### Performance Rules
|
|
662
|
+
|
|
663
|
+
```
|
|
664
|
+
✅ Use useMotionValue instead of useState for animation-driven values
|
|
665
|
+
→ useMotionValue does NOT trigger React re-renders
|
|
666
|
+
|
|
667
|
+
✅ Use useTransform to derive values from motion values
|
|
668
|
+
→ Avoids recomputation in the React render cycle
|
|
669
|
+
|
|
670
|
+
✅ Animate transform properties (x, y, scale, rotate, opacity)
|
|
671
|
+
→ These are GPU-composited and do not trigger layout
|
|
672
|
+
|
|
673
|
+
✅ Use LazyMotion + m.div in production to reduce bundle size
|
|
674
|
+
→ Cuts Framer Motion from ~30KB to ~5KB
|
|
675
|
+
|
|
676
|
+
❌ Do NOT animate width, height, top, left, padding, margin
|
|
677
|
+
→ These trigger layout recalculation every frame
|
|
678
|
+
|
|
679
|
+
❌ Do NOT create new motion values inside render
|
|
680
|
+
→ Causes GC pressure and breaks animation continuity
|
|
681
|
+
|
|
682
|
+
❌ Do NOT nest AnimatePresence unnecessarily
|
|
683
|
+
→ Each instance adds overhead to the reconciler
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
---
|
|
687
|
+
|
|
688
|
+
## Framework Considerations
|
|
689
|
+
|
|
690
|
+
### Next.js (App Router)
|
|
691
|
+
|
|
692
|
+
```tsx
|
|
693
|
+
// Framer Motion components MUST be client components
|
|
694
|
+
"use client"; // ← required at top of file
|
|
695
|
+
|
|
696
|
+
import { motion } from "framer-motion";
|
|
697
|
+
|
|
698
|
+
// ❌ HALLUCINATION TRAP: motion.div CANNOT be used in Server Components
|
|
699
|
+
// The `motion` component requires browser APIs (DOM, window, RAF)
|
|
700
|
+
|
|
701
|
+
// For server-rendered pages with client animations,
|
|
702
|
+
// split into a server-rendered layout + client animation wrapper
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
### Cleanup on Unmount
|
|
706
|
+
|
|
707
|
+
```tsx
|
|
708
|
+
// AnimatePresence handles unmount animations automatically
|
|
709
|
+
// But if using useAnimate or manual animation controls:
|
|
710
|
+
|
|
711
|
+
useEffect(() => {
|
|
712
|
+
const controls = animate(".element", { opacity: 1 });
|
|
713
|
+
|
|
714
|
+
return () => {
|
|
715
|
+
controls.stop(); // ← ALWAYS stop animations on cleanup
|
|
716
|
+
};
|
|
717
|
+
}, []);
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
### Vue / Non-React Usage
|
|
721
|
+
|
|
722
|
+
```
|
|
723
|
+
⚠️ Framer Motion is React-ONLY.
|
|
724
|
+
|
|
725
|
+
For Vue, use:
|
|
726
|
+
- @vueuse/motion (Vue 3 motion library, similar API)
|
|
727
|
+
- vue-kinesis (gesture-based)
|
|
728
|
+
|
|
729
|
+
For Svelte, use:
|
|
730
|
+
- svelte/animate and svelte/transition (built-in)
|
|
731
|
+
- Motion One (framework-agnostic, by Framer Motion creator)
|
|
732
|
+
|
|
733
|
+
For vanilla JS, use:
|
|
734
|
+
- Motion One (motion.dev) — from the same team, but framework-agnostic
|
|
735
|
+
- GSAP — use the gsap-expert skill instead
|
|
736
|
+
|
|
737
|
+
// ❌ HALLUCINATION TRAP: There is NO "framer-motion/vue" or "framer-motion/svelte"
|
|
738
|
+
// Framer Motion is exclusively a React library
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
---
|
|
742
|
+
|
|
743
|
+
## Common Animation Patterns
|
|
744
|
+
|
|
745
|
+
### Staggered List Entrance
|
|
746
|
+
|
|
747
|
+
```tsx
|
|
748
|
+
const container = {
|
|
749
|
+
hidden: {},
|
|
750
|
+
visible: {
|
|
751
|
+
transition: { staggerChildren: 0.08, delayChildren: 0.1 },
|
|
752
|
+
},
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
const item = {
|
|
756
|
+
hidden: { opacity: 0, y: 20, filter: "blur(4px)" },
|
|
757
|
+
visible: {
|
|
758
|
+
opacity: 1,
|
|
759
|
+
y: 0,
|
|
760
|
+
filter: "blur(0px)",
|
|
761
|
+
transition: { duration: 0.4, ease: [0.25, 0.46, 0.45, 0.94] },
|
|
762
|
+
},
|
|
763
|
+
};
|
|
764
|
+
|
|
765
|
+
<motion.ul variants={container} initial="hidden" animate="visible">
|
|
766
|
+
{list.map((entry) => (
|
|
767
|
+
<motion.li key={entry.id} variants={item}>
|
|
768
|
+
{entry.name}
|
|
769
|
+
</motion.li>
|
|
770
|
+
))}
|
|
771
|
+
</motion.ul>
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
### Smooth Tab Indicator
|
|
775
|
+
|
|
776
|
+
```tsx
|
|
777
|
+
function Tabs({ tabs, activeTab }) {
|
|
778
|
+
return (
|
|
779
|
+
<div className="tab-list">
|
|
780
|
+
{tabs.map((tab) => (
|
|
781
|
+
<button
|
|
782
|
+
key={tab.id}
|
|
783
|
+
onClick={() => setActiveTab(tab.id)}
|
|
784
|
+
className="tab"
|
|
785
|
+
>
|
|
786
|
+
{tab.label}
|
|
787
|
+
{activeTab === tab.id && (
|
|
788
|
+
<motion.div
|
|
789
|
+
layoutId="tab-indicator" // ← shared element
|
|
790
|
+
className="tab-underline"
|
|
791
|
+
transition={{ type: "spring", stiffness: 500, damping: 30 }}
|
|
792
|
+
/>
|
|
793
|
+
)}
|
|
794
|
+
</button>
|
|
795
|
+
))}
|
|
796
|
+
</div>
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
### Card Hover Effect
|
|
802
|
+
|
|
803
|
+
```tsx
|
|
804
|
+
<motion.div
|
|
805
|
+
whileHover={{ y: -4, boxShadow: "0 20px 40px rgba(0,0,0,0.15)" }}
|
|
806
|
+
transition={{ type: "spring", stiffness: 300, damping: 20 }}
|
|
807
|
+
className="card"
|
|
808
|
+
>
|
|
809
|
+
<motion.img
|
|
810
|
+
whileHover={{ scale: 1.03 }}
|
|
811
|
+
transition={{ duration: 0.3 }}
|
|
812
|
+
src={imageUrl}
|
|
813
|
+
/>
|
|
814
|
+
</motion.div>
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
### Expandable Accordion
|
|
818
|
+
|
|
819
|
+
```tsx
|
|
820
|
+
function Accordion({ title, children, isOpen, onToggle }) {
|
|
821
|
+
return (
|
|
822
|
+
<div>
|
|
823
|
+
<button onClick={onToggle}>{title}</button>
|
|
824
|
+
<AnimatePresence initial={false}>
|
|
825
|
+
{isOpen && (
|
|
826
|
+
<motion.div
|
|
827
|
+
key="content"
|
|
828
|
+
initial={{ height: 0, opacity: 0 }}
|
|
829
|
+
animate={{ height: "auto", opacity: 1 }}
|
|
830
|
+
exit={{ height: 0, opacity: 0 }}
|
|
831
|
+
transition={{ duration: 0.3, ease: [0.04, 0.62, 0.23, 0.98] }}
|
|
832
|
+
style={{ overflow: "hidden" }}
|
|
833
|
+
>
|
|
834
|
+
{children}
|
|
835
|
+
</motion.div>
|
|
836
|
+
)}
|
|
837
|
+
</AnimatePresence>
|
|
838
|
+
</div>
|
|
839
|
+
);
|
|
840
|
+
}
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
### Scroll Progress Bar
|
|
844
|
+
|
|
845
|
+
```tsx
|
|
846
|
+
function ScrollProgressBar() {
|
|
847
|
+
const { scrollYProgress } = useScroll();
|
|
848
|
+
|
|
849
|
+
return (
|
|
850
|
+
<motion.div
|
|
851
|
+
style={{
|
|
852
|
+
scaleX: scrollYProgress,
|
|
853
|
+
transformOrigin: "left",
|
|
854
|
+
position: "fixed",
|
|
855
|
+
top: 0,
|
|
856
|
+
left: 0,
|
|
857
|
+
right: 0,
|
|
858
|
+
height: 4,
|
|
859
|
+
background: "linear-gradient(90deg, #06b6d4, #8b5cf6)",
|
|
860
|
+
zIndex: 50,
|
|
861
|
+
}}
|
|
862
|
+
/>
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
---
|
|
868
|
+
|
|
869
|
+
## Output Format
|
|
870
|
+
|
|
871
|
+
When this skill produces or reviews code, structure your output as follows:
|
|
872
|
+
|
|
873
|
+
```
|
|
874
|
+
━━━ Framer Motion Expert Report ━━━━━━━━━━━━━━━━━━━━━━━━
|
|
875
|
+
Skill: Framer Motion Expert
|
|
876
|
+
FM Version: 12+
|
|
877
|
+
Scope: [N files · N components]
|
|
878
|
+
─────────────────────────────────────────────────
|
|
879
|
+
✅ Passed: [checks that passed, or "All clean"]
|
|
880
|
+
⚠️ Warnings: [non-blocking issues, or "None"]
|
|
881
|
+
❌ Blocked: [blocking issues requiring fix, or "None"]
|
|
882
|
+
─────────────────────────────────────────────────
|
|
883
|
+
VBC status: PENDING → VERIFIED
|
|
884
|
+
Evidence: [test output / lint pass / compile success]
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
**VBC (Verification-Before-Completion) is mandatory.**
|
|
888
|
+
Do not mark status as VERIFIED until concrete terminal evidence is provided.
|
|
889
|
+
|
|
890
|
+
---
|
|
891
|
+
|
|
892
|
+
## 🤖 LLM-Specific Traps
|
|
893
|
+
|
|
894
|
+
AI coding assistants often fall into specific bad habits when generating Framer Motion code. These are strictly forbidden:
|
|
895
|
+
|
|
896
|
+
1. **Inventing Component Names:** There is no `<Motion>`, `<MotionDiv>`, `<AnimatedDiv>`, or `motion()` wrapper function. The correct API is `motion.div`, `motion.span`, etc. (lowercase dot notation).
|
|
897
|
+
2. **Using Deprecated `exitBeforeEnter`:** This prop was removed in Framer Motion 7+. The correct replacement is `mode="wait"` on `AnimatePresence`.
|
|
898
|
+
3. **Missing `key` in AnimatePresence:** Every direct child of `AnimatePresence` MUST have a unique `key` prop for exit animations to work.
|
|
899
|
+
4. **Using `motion.div` in Server Components:** Framer Motion requires browser APIs. Components using `motion` MUST be marked `"use client"` in Next.js App Router.
|
|
900
|
+
5. **Confusing `useAnimation` (removed) with `useAnimate`:** The modern imperative API is `useAnimate()` which returns `[scope, animate]`, NOT `useAnimation()` which returned animation controls.
|
|
901
|
+
6. **Using `m.div` Without `LazyMotion`:** The `m` component is only valid inside a `<LazyMotion>` provider. Using it standalone produces no animations.
|
|
902
|
+
7. **Layout Animations Without `domMax`:** The `layout` prop and `layoutId` require `domMax` features in LazyMotion, not `domAnimation`.
|
|
903
|
+
8. **Over-Animating:** Adding spring animations to every element creates visual chaos. Be intentional — every animation must serve a UX purpose.
|
|
904
|
+
9. **Hallucinating Non-React Exports:** There is no `framer-motion/vue`, `framer-motion/svelte`, or `framer-motion/vanilla`. It is React-only.
|
|
905
|
+
10. **Animating Layout Properties:** Animating `width`, `height`, `top`, `left` directly instead of using `x`, `y`, `scale`, or the `layout` prop causes layout thrashing.
|
|
906
|
+
|
|
907
|
+
---
|
|
908
|
+
|
|
909
|
+
## 🏛️ Tribunal Integration (Anti-Hallucination)
|
|
910
|
+
|
|
911
|
+
**Slash command: `/review` or `/tribunal-full`**
|
|
912
|
+
**Active reviewers: `logic-reviewer` · `security-auditor` · `frontend-reviewer` · `performance-optimizer`**
|
|
913
|
+
|
|
914
|
+
### ❌ Forbidden AI Tropes
|
|
915
|
+
|
|
916
|
+
1. **Blind Assumptions:** Never make an assumption without documenting it clearly with `// VERIFY: [reason]`.
|
|
917
|
+
2. **Silent Degradation:** Catching and suppressing animation errors without logging.
|
|
918
|
+
3. **Context Amnesia:** Forgetting the user's constraints (e.g., generating `motion.div` in a Server Component).
|
|
919
|
+
4. **Accessibility Neglect:** Failing to implement `prefers-reduced-motion` support is a hard block.
|
|
920
|
+
|
|
921
|
+
### ✅ Pre-Flight Self-Audit
|
|
922
|
+
|
|
923
|
+
Review these questions before confirming output:
|
|
924
|
+
```
|
|
925
|
+
✅ Did I use "use client" for all components using motion.*?
|
|
926
|
+
✅ Did I add a unique `key` to every AnimatePresence child?
|
|
927
|
+
✅ Did I use mode="wait" instead of the removed exitBeforeEnter?
|
|
928
|
+
✅ Did I use useMotionValue (not useState) for animation values?
|
|
929
|
+
✅ Did I animate transforms (x, y, scale) instead of layout props?
|
|
930
|
+
✅ Did I handle prefers-reduced-motion with useReducedMotion()?
|
|
931
|
+
✅ Did I use LazyMotion + m.div for bundle optimization?
|
|
932
|
+
✅ Did I stop/cleanup animations on unmount?
|
|
933
|
+
```
|
|
934
|
+
|
|
935
|
+
### 🛑 Verification-Before-Completion (VBC) Protocol
|
|
936
|
+
|
|
937
|
+
**CRITICAL:** You must follow a strict "evidence-based closeout" state machine.
|
|
938
|
+
- ❌ **Forbidden:** Declaring a task complete because the output "looks correct."
|
|
939
|
+
- ✅ **Required:** You are explicitly forbidden from finalizing any task without providing **concrete evidence** (terminal output, passing tests, compile success, or equivalent proof) that your output works as intended.
|