diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json
index 106ace1c..5bbc44bc 100644
--- a/.claude-plugin/marketplace.json
+++ b/.claude-plugin/marketplace.json
@@ -11,7 +11,7 @@
{
"name": "ecc",
"source": "./",
- "description": "The most comprehensive Claude Code plugin — 58 agents, 217 skills, 74 legacy command shims, selective install profiles, and production-ready hooks for TDD, security scanning, code review, and continuous learning",
+ "description": "The most comprehensive Claude Code plugin — 58 agents, 220 skills, 74 legacy command shims, selective install profiles, and production-ready hooks for TDD, security scanning, code review, and continuous learning",
"version": "2.0.0-rc.1",
"author": {
"name": "Affaan Mustafa",
diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json
index 3df4b3c2..fb77231f 100644
--- a/.claude-plugin/plugin.json
+++ b/.claude-plugin/plugin.json
@@ -1,7 +1,7 @@
{
"name": "ecc",
"version": "2.0.0-rc.1",
- "description": "Battle-tested Claude Code plugin for engineering teams — 58 agents, 217 skills, 74 legacy command shims, production-ready hooks, and selective install workflows evolved through continuous real-world use",
+ "description": "Battle-tested Claude Code plugin for engineering teams — 58 agents, 220 skills, 74 legacy command shims, production-ready hooks, and selective install workflows evolved through continuous real-world use",
"author": {
"name": "Affaan Mustafa",
"url": "https://x.com/affaanmustafa"
diff --git a/AGENTS.md b/AGENTS.md
index a8daa5c1..03a9b322 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,6 +1,6 @@
# Everything Claude Code (ECC) — Agent Instructions
-This is a **production-ready AI coding plugin** providing 58 specialized agents, 217 skills, 74 commands, and automated hook workflows for software development.
+This is a **production-ready AI coding plugin** providing 58 specialized agents, 220 skills, 74 commands, and automated hook workflows for software development.
**Version:** 2.0.0-rc.1
@@ -148,7 +148,7 @@ Troubleshoot failures: check test isolation → verify mocks → fix implementat
```
agents/ — 58 specialized subagents
-skills/ — 217 workflow skills and domain knowledge
+skills/ — 220 workflow skills and domain knowledge
commands/ — 74 slash commands
hooks/ — Trigger-based automations
rules/ — Always-follow guidelines (common + per-language)
diff --git a/README.md b/README.md
index c385a2f3..4f370dc2 100644
--- a/README.md
+++ b/README.md
@@ -358,7 +358,7 @@ If you stacked methods, clean up in this order:
/plugin list ecc@ecc
```
-**That's it!** You now have access to 58 agents, 217 skills, and 74 legacy command shims.
+**That's it!** You now have access to 58 agents, 220 skills, and 74 legacy command shims.
### Dashboard GUI
@@ -1362,7 +1362,7 @@ The configuration is automatically detected from `.opencode/opencode.json`.
|---------|-------------|----------|--------|
| Agents | PASS: 58 agents | PASS: 12 agents | **Claude Code leads** |
| Commands | PASS: 74 commands | PASS: 35 commands | **Claude Code leads** |
-| Skills | PASS: 217 skills | PASS: 37 skills | **Claude Code leads** |
+| Skills | PASS: 220 skills | PASS: 37 skills | **Claude Code leads** |
| Hooks | PASS: 8 event types | PASS: 11 events | **OpenCode has more!** |
| Rules | PASS: 29 rules | PASS: 13 instructions | **Claude Code leads** |
| MCP Servers | PASS: 14 servers | PASS: Full | **Full parity** |
@@ -1467,7 +1467,7 @@ ECC is the **first plugin to maximize every major AI coding tool**. Here's how e
|---------|------------|------------|-----------|----------|
| **Agents** | 58 | Shared (AGENTS.md) | Shared (AGENTS.md) | 12 |
| **Commands** | 74 | Shared | Instruction-based | 35 |
-| **Skills** | 217 | Shared | 10 (native format) | 37 |
+| **Skills** | 220 | Shared | 10 (native format) | 37 |
| **Hook Events** | 8 types | 15 types | None yet | 11 types |
| **Hook Scripts** | 20+ scripts | 16 scripts (DRY adapter) | N/A | Plugin hooks |
| **Rules** | 34 (common + lang) | 34 (YAML frontmatter) | Instruction-based | 13 instructions |
diff --git a/README.zh-CN.md b/README.zh-CN.md
index c420c2f7..e31822dd 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -160,7 +160,7 @@ Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/"
/plugin list ecc@ecc
```
-**完成!** 你现在可以使用 58 个代理、217 个技能和 74 个命令。
+**完成!** 你现在可以使用 58 个代理、220 个技能和 74 个命令。
### multi-* 命令需要额外配置
diff --git a/docs/zh-CN/AGENTS.md b/docs/zh-CN/AGENTS.md
index 25d7494e..a4fac6e9 100644
--- a/docs/zh-CN/AGENTS.md
+++ b/docs/zh-CN/AGENTS.md
@@ -1,6 +1,6 @@
# Everything Claude Code (ECC) — 智能体指令
-这是一个**生产就绪的 AI 编码插件**,提供 58 个专业代理、217 项技能、74 条命令以及自动化钩子工作流,用于软件开发。
+这是一个**生产就绪的 AI 编码插件**,提供 58 个专业代理、220 项技能、74 条命令以及自动化钩子工作流,用于软件开发。
**版本:** 2.0.0-rc.1
@@ -147,7 +147,7 @@
```
agents/ — 58 个专业子代理
-skills/ — 217 个工作流技能和领域知识
+skills/ — 220 个工作流技能和领域知识
commands/ — 74 个斜杠命令
hooks/ — 基于触发的自动化
rules/ — 始终遵循的指导方针(通用 + 每种语言)
diff --git a/docs/zh-CN/README.md b/docs/zh-CN/README.md
index 1d4c1adb..437f2f95 100644
--- a/docs/zh-CN/README.md
+++ b/docs/zh-CN/README.md
@@ -224,7 +224,7 @@ Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/"
/plugin list ecc@ecc
```
-**搞定!** 你现在可以使用 58 个智能体、217 项技能和 74 个命令了。
+**搞定!** 你现在可以使用 58 个智能体、220 项技能和 74 个命令了。
***
@@ -1134,7 +1134,7 @@ opencode
|---------|-------------|----------|--------|
| 智能体 | PASS: 58 个 | PASS: 12 个 | **Claude Code 领先** |
| 命令 | PASS: 74 个 | PASS: 35 个 | **Claude Code 领先** |
-| 技能 | PASS: 217 项 | PASS: 37 项 | **Claude Code 领先** |
+| 技能 | PASS: 220 项 | PASS: 37 项 | **Claude Code 领先** |
| 钩子 | PASS: 8 种事件类型 | PASS: 11 种事件 | **OpenCode 更多!** |
| 规则 | PASS: 29 条 | PASS: 13 条指令 | **Claude Code 领先** |
| MCP 服务器 | PASS: 14 个 | PASS: 完整 | **完全对等** |
@@ -1242,7 +1242,7 @@ ECC 是**第一个最大化利用每个主要 AI 编码工具的插件**。以
|---------|------------|------------|-----------|----------|
| **智能体** | 58 | 共享 (AGENTS.md) | 共享 (AGENTS.md) | 12 |
| **命令** | 74 | 共享 | 基于指令 | 35 |
-| **技能** | 217 | 共享 | 10 (原生格式) | 37 |
+| **技能** | 220 | 共享 | 10 (原生格式) | 37 |
| **钩子事件** | 8 种类型 | 15 种类型 | 暂无 | 11 种类型 |
| **钩子脚本** | 20+ 个脚本 | 16 个脚本 (DRY 适配器) | N/A | 插件钩子 |
| **规则** | 34 (通用 + 语言) | 34 (YAML 前页) | 基于指令 | 13 条指令 |
diff --git a/skills/motion-advanced/SKILL.md b/skills/motion-advanced/SKILL.md
new file mode 100644
index 00000000..3b4116b7
--- /dev/null
+++ b/skills/motion-advanced/SKILL.md
@@ -0,0 +1,575 @@
+---
+name: motion-advanced
+description: Advanced motion patterns for React / Next.js — drag & drop, gestures, text animations, SVG path drawing, custom hooks, imperative sequences (useAnimate), loaders, and the full API decision tree. Requires motion-foundations.
+version: 1.0
+tags: [motion, animation, advanced, gestures, svg]
+category: frontend
+author: jeff
+---
+
+# Motion Advanced
+
+Complex, interactive, and physics-based animation patterns.
+Requires `motion-foundations` to be set up first.
+Use these when `motion-patterns` is not enough.
+
+## When to Activate
+
+- Building drag-to-dismiss sheets, swipe gestures, or reorderable lists
+- Animating text word-by-word, character-by-character, or as a live counter
+- Drawing SVG paths, morphing icons, or animating circular progress
+- Writing a custom animation hook (`useScrollReveal`, magnetic button, cursor follower)
+- Sequencing multi-step animations imperatively with `useAnimate`
+- Building spinners, shimmer skeletons, pulse indicators, or loading button states
+
+## Outputs
+
+This skill produces:
+
+- Drag interactions: draggable cards, drag-to-dismiss sheets, `Reorder.Group` lists
+- Gesture hooks: swipe detection, long press, pinch outline
+- Text animation components: word reveal, character typewriter, number counter
+- SVG animation: path draw-on, icon morph, stroke progress ring
+- Custom hooks: `useScrollReveal`, `useHoverScale`, `useNavigationDirection`, `useInViewOnce`
+- Imperative sequences via `useAnimate` with interrupt-safe `async/await`
+- Loader components: spinner, shimmer, pulse dot, progress bar, button loading state
+
+## Principles
+
+- Physics-based motion (`useSpring`, `springs.*`) always feels more natural than duration-based for direct manipulation.
+- `useMotionValue` + `useTransform` computes derived values without triggering re-renders.
+- `useAnimate` sequences are imperative and interrupt-safe — calling `animate()` mid-flight cancels the previous animation automatically.
+- Motion values (`useMotionValue`, `useSpring`) are SSR-safe and do not cause hydration errors.
+
+## Rules
+
+1. **Drag interactions must be tested on touch devices**, not just mouse. `drag` prop works on both but feel and threshold differ.
+2. **Infinite animations must pause when `document.visibilityState === "hidden"`.** Background tabs must not consume GPU/CPU.
+3. **Swipe threshold must be explicit.** Never infer intent from velocity alone; combine `offset` + `velocity` checks.
+4. **`useAnimate` scope ref must be attached to a mounted DOM element.** Calling `animate()` before mount throws silently.
+5. **Motion values must not be recreated on render.** `useMotionValue(0)` inside a component body is correct; `new MotionValue(0)` in a render is not.
+6. **All token values are imported from `motion-foundations`.** No inline numbers.
+7. **Custom hooks must handle cleanup.** Every `window.addEventListener` needs a matching `removeEventListener` in the `useEffect` return.
+8. **SVG morphing requires equal path command counts.** Paths with different command structures snap instead of interpolating.
+
+## Decision Guidance
+
+### Choosing the right advanced API
+
+| Scenario | API |
+| ------------------------------ | -------------------------------- |
+| Drag with physics on release | `drag` + `dragTransition: springs.release` |
+| Ordered drag-to-reorder list | `Reorder.Group` + `Reorder.Item` |
+| Dismiss on drag offset | `drag="y"` + `onDragEnd` offset check |
+| Swipe left/right | `drag="x"` + `onDragEnd` offset check |
+| Long press | `useLongPress` hook |
+| Value smoothed over time | `useSpring` |
+| Value derived from another | `useTransform` |
+| Multi-step sequence | `useAnimate` with `async/await` |
+| One-shot imperative animation | `animate()` from `motion` |
+| Text entering word by word | Stagger on `inline-block` spans |
+| SVG drawing on | `pathLength` 0 → 1 |
+| SVG morph | `d` attribute tween (equal commands) |
+| Circular progress | `strokeDashoffset` tween |
+
+### When to use `useSpring` vs a spring transition
+
+| | `useSpring` | `transition: springs.*` |
+| -------------- | ---------------------------------------- | ----------------------- |
+| Use for | Cursor follower, pointer-tracked values | Discrete state changes |
+| Updates | Continuous, on every frame | Triggered by state change |
+| Interrupt | Smooth — physics picks up from velocity | Restarts from current value |
+
+## Core Concepts
+
+### useMotionValue + useTransform
+
+Reactive computation without re-renders:
+
+```tsx
+const x = useMotionValue(0)
+const opacity = useTransform(x, [-200, 0, 200], [0, 1, 0])
+// opacity updates every frame as x changes — no setState, no re-render
+```
+
+### useAnimate
+
+Returns `[scope, animate]`. The scope ref must be attached to a DOM element.
+`animate()` calls are interrupt-safe — calling mid-flight cancels the previous run.
+
+```tsx
+const [scope, animate] = useAnimate()
+
+async function play() {
+ await animate(".step-1", { opacity: 1 }, { duration: 0.3 })
+ await animate(".step-2", { x: 0 }, { duration: 0.4 })
+ animate(".step-3", { scale: 1 }, { duration: 0.25 }) // fire and forget
+}
+
+return
...
+```
+
+## Code Examples
+
+### Draggable card
+
+```tsx
+"use client"
+import { motion } from "motion/react"
+import { springs, motionTokens } from "@/lib/motion-tokens"
+
+
+```
+
+### Drag-to-dismiss sheet
+
+```tsx
+"use client"
+import { motion, useMotionValue, useTransform } from "motion/react"
+
+export function BottomSheet({ onClose }: { onClose: () => void }) {
+ const y = useMotionValue(0)
+ const opacity = useTransform(y, [0, 200], [1, 0])
+
+ return (
+ {
+ // Rule 3: combine offset + velocity
+ if (info.offset.y > 120 || info.velocity.y > 500) onClose()
+ }}
+ />
+ )
+}
+```
+
+### Reorderable list
+
+```tsx
+"use client"
+import { Reorder } from "motion/react"
+
+export function SortableList() {
+ const [items, setItems] = useState(initialItems)
+ return (
+
+ {items.map((item) => (
+
+ {item.label}
+
+ ))}
+
+ )
+}
+```
+
+### Swipe detection
+
+```tsx
+"use client"
+import { motion } from "motion/react"
+
+const OFFSET_THRESHOLD = 50
+const VELOCITY_THRESHOLD = 300
+
+ {
+ const swipedRight = info.offset.x > OFFSET_THRESHOLD || info.velocity.x > VELOCITY_THRESHOLD
+ const swipedLeft = info.offset.x < -OFFSET_THRESHOLD || info.velocity.x < -VELOCITY_THRESHOLD
+ if (swipedRight) onSwipeRight()
+ if (swipedLeft) onSwipeLeft()
+ }}
+/>
+```
+
+### Long press hook
+
+```tsx
+import { useRef } from "react"
+
+export function useLongPress(callback: () => void, ms = 600) {
+ const timerRef = useRef>()
+ return {
+ onPointerDown: () => { timerRef.current = setTimeout(callback, ms) },
+ onPointerUp: () => clearTimeout(timerRef.current),
+ onPointerLeave: () => clearTimeout(timerRef.current),
+ }
+}
+```
+
+### Word-by-word reveal
+
+```tsx
+"use client"
+import { motion } from "motion/react"
+import { springs } from "@/lib/motion-tokens"
+
+export function AnimatedText({ text }: { text: string }) {
+ return (
+
+ {text.split(" ").map((word, i) => (
+
+ {word}
+
+ ))}
+
+ )
+}
+```
+
+### Number counter
+
+```tsx
+"use client"
+import { useRef, useEffect } from "react"
+import { animate } from "motion"
+import { motionTokens } from "@/lib/motion-tokens"
+
+export function Counter({ to }: { to: number }) {
+ const nodeRef = useRef(null)
+
+ useEffect(() => {
+ const controls = animate(0, to, {
+ duration: motionTokens.duration.crawl,
+ ease: motionTokens.easing.smooth,
+ onUpdate: (v) => {
+ if (nodeRef.current) nodeRef.current.textContent = Math.round(v).toString()
+ },
+ })
+ return controls.stop // Rule 7: cleanup
+ }, [to])
+
+ return
+}
+```
+
+### SVG path draw-on
+
+```tsx
+"use client"
+import { motion } from "motion/react"
+import { motionTokens } from "@/lib/motion-tokens"
+
+
+```
+
+### Stroke progress ring
+
+```tsx
+"use client"
+import { motion } from "motion/react"
+import { motionTokens } from "@/lib/motion-tokens"
+
+const CIRCUMFERENCE = 2 * Math.PI * 40 // r=40
+
+export function ProgressRing({ progress }: { progress: number }) {
+ return (
+
+ )
+}
+```
+
+### useScrollReveal hook
+
+```tsx
+"use client"
+import { useRef } from "react"
+import { useScroll, useTransform } from "motion/react"
+import { motionTokens } from "@/lib/motion-tokens"
+
+export function useScrollReveal() {
+ const ref = useRef(null)
+ const { scrollYProgress } = useScroll({ target: ref, offset: ["start end", "end start"] })
+ const opacity = useTransform(scrollYProgress, [0, 0.3], [0, 1])
+ const y = useTransform(scrollYProgress, [0, 0.3], [motionTokens.distance.lg, 0])
+ return { ref, style: { opacity, y } }
+}
+
+// Usage
+const { ref, style } = useScrollReveal()
+
+```
+
+### Cursor follower
+
+```tsx
+"use client"
+import { useEffect } from "react"
+import { motion, useMotionValue, useSpring } from "motion/react"
+
+export function CursorFollower() {
+ const x = useMotionValue(-100)
+ const y = useMotionValue(-100)
+ const sx = useSpring(x, { stiffness: 120, damping: 16 })
+ const sy = useSpring(y, { stiffness: 120, damping: 16 })
+
+ useEffect(() => {
+ const move = (e: MouseEvent) => { x.set(e.clientX); y.set(e.clientY) }
+ window.addEventListener("mousemove", move)
+ return () => window.removeEventListener("mousemove", move) // Rule 7
+ }, [])
+
+ return (
+
+ )
+}
+```
+
+### Shimmer skeleton
+
+```tsx
+"use client"
+import { motion } from "motion/react"
+
+ export function ShimmerSkeleton({ className }: { className?: string }) {
+ const controls = useAnimation()
+ useEffect(() => {
+ const run = () =>
+ controls.start({ x: ["-100%", "100%"], transition: { repeat: Infinity, duration: 1.2, ease: "linear" } })
+ const onVis = () => (document.visibilityState === "hidden" ? controls.stop() : run())
+ run()
+ document.addEventListener("visibilitychange", onVis)
+ return () => document.removeEventListener("visibilitychange", onVis)
+ }, [controls])
+ return (
+
+
+
+ )
+ }
+
+ )
+}
+```
+
+### Button loading state
+
+```tsx
+"use client"
+import { motion, AnimatePresence } from "motion/react"
+import { motionTokens, springs } from "@/lib/motion-tokens"
+
+export function LoadingButton({
+ loading,
+ label,
+ onClick,
+}: {
+ loading: boolean
+ label: string
+ onClick: () => void
+}) {
+ return (
+
+
+ {loading ? (
+
+ …
+
+ ) : (
+
+ {label}
+
+ )}
+
+
+ )
+}
+```
+
+### Infinite animation with visibility pause
+
+```tsx
+"use client"
+import { useEffect, useRef } from "react"
+import { motion, useAnimation } from "motion/react"
+
+export function PulseDot() {
+ const controls = useAnimation()
+
+ useEffect(() => {
+ const pulse = () =>
+ controls.start({
+ scale: [1, 1.4, 1],
+ opacity: [1, 0.6, 1],
+ transition: { repeat: Infinity, duration: 1.8 },
+ })
+
+ // Rule 2: pause when tab is hidden
+ const handleVisibility = () => {
+ if (document.visibilityState === "hidden") controls.stop()
+ else pulse()
+ }
+
+ pulse()
+ document.addEventListener("visibilitychange", handleVisibility)
+ return () => document.removeEventListener("visibilitychange", handleVisibility) // Rule 7
+ }, [])
+
+ return
+}
+```
+
+## End-to-End Example
+
+Drag-to-dismiss sheet with shimmer content, loading state, and reduced motion
+support — combining `useMotionValue`, `useTransform`, `useSafeMotion`,
+`AnimatePresence`, and tokens from `motion-foundations`:
+
+```tsx
+"use client"
+import { useState } from "react"
+import { motion, AnimatePresence, useMotionValue, useTransform } from "motion/react"
+import { springs, motionTokens } from "@/lib/motion-tokens"
+import { useSafeMotion } from "@/hooks/use-reduced-motion"
+import { ShimmerSkeleton } from "./shimmer-skeleton"
+
+export function DismissibleSheet({
+ isOpen,
+ onClose,
+ loading,
+ children,
+}: {
+ isOpen: boolean
+ onClose: () => void
+ loading: boolean
+ children: React.ReactNode
+}) {
+ const safe = useSafeMotion(motionTokens.distance.xl)
+ const y = useMotionValue(0)
+ const opacity = useTransform(y, [0, 200], [1, 0])
+
+ return (
+
+ {isOpen && (
+ <>
+ {/* Backdrop */}
+
+
+ {/* Sheet — drag-to-dismiss */}
+ {
+ if (info.offset.y > 120 || info.velocity.y > 500) onClose()
+ }}
+ initial={safe.initial}
+ animate={safe.animate}
+ exit={safe.exit}
+ transition={springs.gentle}
+ >
+ {loading ? (
+
+
+
+
+
+ ) : children}
+
+ >
+ )}
+
+ )
+}
+```
+
+## Constraints / Non-Goals
+
+This skill does **not** cover:
+
+- Token and spring definitions → see `motion-foundations`
+- Standard UI patterns (button, modal, stagger, page transitions) → see `motion-patterns`
+- CSS-only animations or Tailwind `animate-*` without `motion/react`
+- Canvas or WebGL-based animation (Three.js, Pixi, etc.)
+- Full drag-and-drop systems with external state managers (dnd-kit, react-beautiful-dnd)
+- Game-loop or frame-by-frame animation
+
+## Anti-Patterns
+
+| Anti-pattern | Rule violated | Fix |
+| ---------------------------------------------- | ------- | ------------------------------------------------ |
+| `drag` tested only on desktop | Rule 1 | Test on touch emulator and real device |
+| `animate={{ repeat: Infinity }}` with no pause | Rule 2 | Add `visibilitychange` listener |
+| `onDragEnd` checking only offset, not velocity | Rule 3 | Check both `info.offset` and `info.velocity` |
+| `animate(scope, ...)` before `useEffect` | Rule 4 | Call `animate()` only after mount |
+| `const x = new MotionValue(0)` in render | Rule 5 | Use `const x = useMotionValue(0)` |
+| `transition={{ duration: 1.2 }}` inline | Rule 6 | Use `motionTokens.duration.crawl` |
+| `useEffect` without cleanup | Rule 7 | Return `removeEventListener` / `controls.stop` |
+| SVG morph between paths with different commands | Rule 8 | Normalize path commands before animating |
+
+## Related Skills
+
+- **`motion-foundations`** — defines all tokens, springs, `useSafeMotion`, and SSR guards imported here. Must be set up before using this skill.
+- **`motion-patterns`** — handles standard UI patterns (button, modal, stagger, page transitions, scroll reveals). Use it before reaching for the advanced patterns here.
diff --git a/skills/motion-foundations/SKILL.md b/skills/motion-foundations/SKILL.md
new file mode 100644
index 00000000..94521f1b
--- /dev/null
+++ b/skills/motion-foundations/SKILL.md
@@ -0,0 +1,291 @@
+---
+name: motion-foundations
+description: Motion tokens, spring presets, performance rules, device adaptation, accessibility enforcement, and SSR safety for React / Next.js using motion/react. Foundation layer — all other motion skills depend on this.
+version: 1.0
+tags: [motion, animation, performance, accessibility]
+category: frontend
+author: jeff
+---
+
+# Motion Foundations
+
+The base layer of the motion system. Defines every value, constraint, and
+rule that downstream skills (`motion-patterns`, `motion-advanced`) inherit.
+Load this skill before any animation work begins.
+
+## When to Activate
+
+- Starting any animated component from scratch
+- Setting up tokens, spring presets, or easing values
+- Implementing `prefers-reduced-motion` support
+- Debugging hydration mismatches from animation initial states
+- Evaluating whether an animation should exist at all
+
+## Outputs
+
+This skill produces:
+
+- A shared `motionTokens` object (duration, easing, distance, scale)
+- A shared `springs` preset map (5 named configs)
+- A `shouldAnimate` boolean gate used by all components
+- Accessibility-compliant animation defaults via `useReducedMotion`
+- SSR-safe initial states with zero hydration warnings
+
+## Principles
+
+Motion must do at least one of the following or it must be removed:
+
+- Guide attention
+- Communicate state
+- Preserve spatial continuity
+
+Responsiveness always outranks smoothness. A 60 fps animation that causes
+input delay is worse than no animation.
+
+## Rules
+
+These are non-negotiable. They apply to every component in the system.
+
+1. **Use `motion/react` only.** Never import from `framer-motion`. Never mix the two in the same tree.
+2. **`initial` must match server output.** If the server renders `opacity: 1`, the `initial` prop must also be `opacity: 1`. No exceptions.
+3. **Reduced motion overrides everything.** When `useReducedMotion()` returns `true` or `prefersReduced` is `true`, all transforms are disabled. Opacity-only fades at ≤ 0.2s are the only permitted fallback.
+4. **Never animate layout properties.** `width`, `height`, `top`, `left`, `margin`, `padding` are banned from `animate`. Use `transform` and `opacity` only.
+5. **All token values come from `motionTokens`.** Hardcoded durations and easings in component files are forbidden.
+6. **All spring configs come from the `springs` map.** Inline `stiffness`/`damping` values are forbidden.
+7. **`"use client"` is required** on every file that imports from `motion/react`.
+8. **Never read `window` or `navigator` at module level.** Always guard with `typeof window !== "undefined"`.
+
+## Decision Guidance
+
+### Choosing a duration
+
+| Token | Use when |
+| --------- | -------------------------------------------- |
+| `instant` | Tooltip show/hide, focus ring, badge update |
+| `fast` | Button feedback, icon swap, chip toggle |
+| `normal` | Modal open, card expand, page element enter |
+| `slow` | Hero entrance, full-page transition |
+| `crawl` | Deliberate storytelling; use sparingly |
+
+### Choosing a spring
+
+| Preset | Use when |
+| --------- | ------------------------------------------ |
+| `snappy` | Default UI — buttons, chips, nav items |
+| `gentle` | Cards, modals, panels landing softly |
+| `bouncy` | Playful moments — empty states, onboarding |
+| `instant` | Tooltips, popovers, dropdowns |
+| `release` | Drag release — natural physics feel |
+
+### When to disable animation entirely
+
+Disable (set `shouldAnimate = false`) when:
+
+- `prefersReduced` is `true`
+- `isLowEnd` is `true` and the animation is non-essential
+- The element is off-screen and will never enter the viewport
+- The animation is purely decorative with no UX purpose
+
+## Core Concepts
+
+### Token system
+
+```ts
+// lib/motion-tokens.ts
+export const motionTokens = {
+ duration: {
+ instant: 0.08,
+ fast: 0.18,
+ normal: 0.35,
+ slow: 0.6,
+ crawl: 1.0,
+ },
+ easing: {
+ smooth: [0.22, 1, 0.36, 1],
+ sharp: [0.4, 0, 0.2, 1],
+ bounce: [0.34, 1.56, 0.64, 1],
+ linear: [0, 0, 1, 1],
+ },
+ distance: {
+ xs: 4,
+ sm: 8,
+ md: 16,
+ lg: 24,
+ xl: 48,
+ },
+ scale: {
+ subtle: 0.98,
+ press: 0.95,
+ pop: 1.04,
+ },
+}
+
+export const springs = {
+ snappy: { type: "spring", stiffness: 300, damping: 30 },
+ gentle: { type: "spring", stiffness: 120, damping: 14 },
+ bouncy: { type: "spring", stiffness: 400, damping: 10 },
+ instant: { type: "spring", stiffness: 600, damping: 35 },
+ release: { type: "spring", stiffness: 200, damping: 20, restDelta: 0.001 },
+}
+```
+
+### Runtime flags
+
+```ts
+// lib/motion-config.ts
+export const motionConfig = {
+ isLowEnd:
+ typeof navigator !== "undefined" &&
+ navigator.hardwareConcurrency <= 4,
+
+ prefersReduced:
+ typeof window !== "undefined" &&
+ window.matchMedia("(prefers-reduced-motion: reduce)").matches,
+
+ get shouldAnimate() {
+ return !this.prefersReduced
+ },
+
+ get duration() {
+ return this.isLowEnd || this.prefersReduced
+ ? motionTokens.duration.instant
+ : motionTokens.duration.normal
+ },
+}
+```
+
+### Accessibility
+
+**Priority order (highest to lowest):**
+
+1. `prefers-reduced-motion: reduce` — disables all transforms, limits opacity transitions to ≤ 0.2s
+2. Low-end device detection — reduces duration, removes non-essential animations
+3. Design preference — everything else
+
+Motion must degrade gracefully. It must never disappear abruptly in a way
+that causes layout shift or confuses orientation.
+
+```tsx
+// hooks/use-reduced-motion.tsx
+"use client"
+import { useReducedMotion } from "motion/react"
+
+export function useSafeMotion(fullY: number = 16) {
+ const reduce = useReducedMotion()
+ return {
+ initial: { opacity: 0, y: reduce ? 0 : fullY },
+ animate: { opacity: 1, y: 0 },
+ exit: { opacity: 0, y: reduce ? 0 : -fullY },
+ }
+}
+```
+
+```css
+/* globals.css */
+@media (prefers-reduced-motion: reduce) {
+ .motion-safe-transition { transition: opacity 0.15s; }
+ .motion-reduce-transform { transform: none !important; }
+}
+```
+
+```html
+
+
+```
+
+### SSR / hydration safety
+
+**Rule: `initial` must always match what the server renders.**
+
+```tsx
+// WRONG — server renders opacity:1 but initial says 0 → hydration mismatch
+
+
+// CORRECT — use AnimatePresence or defer to client mount
+"use client"
+const [mounted, setMounted] = useState(false)
+useEffect(() => setMounted(true), [])
+
+
+```
+
+## Code Examples
+
+### End-to-end: tokens + springs + accessibility + SSR guard
+
+```tsx
+// components/fade-in-card.tsx
+"use client"
+
+import { useState, useEffect } from "react"
+import { motion } from "motion/react"
+import { motionTokens, springs } from "@/lib/motion-tokens"
+import { useSafeMotion } from "@/hooks/use-reduced-motion"
+import { motionConfig } from "@/lib/motion-config"
+
+interface FadeInCardProps {
+ children: React.ReactNode
+ delay?: number
+}
+
+export function FadeInCard({ children, delay = 0 }: FadeInCardProps) {
+ // SSR guard — initial must match server output (opacity: 1)
+ const [mounted, setMounted] = useState(false)
+ useEffect(() => setMounted(true), [])
+
+ // Accessibility — disables transform when reduced motion is preferred
+ const safeMotion = useSafeMotion(motionTokens.distance.md)
+
+ // Device gate — skip animation on low-end hardware
+ if (!motionConfig.shouldAnimate || !mounted) {
+ return {children}
+ }
+
+ return (
+
+ {children}
+
+ )
+}
+```
+
+## Constraints / Non-Goals
+
+This skill does **not** cover:
+
+- UI component patterns (button, modal, stagger) → see `motion-patterns`
+- Drag, gestures, SVG, text animations, custom hooks → see `motion-advanced`
+- CSS-only animations or Tailwind `animate-*` classes without `motion/react`
+- Third-party animation libraries (GSAP, anime.js, etc.)
+- Motion design decisions (when to animate, what to emphasize) — that is a design concern, not a code constraint
+
+## Anti-Patterns
+
+| Anti-pattern | Rule violated | Fix |
+| --------------------------------------- | ------- | ------------------------------- |
+| `import { motion } from "framer-motion"` | Rule 1 | Use `motion/react` |
+| `initial={{ opacity: 0 }}` on SSR component | Rule 2 | Add mount guard |
+| Skipping `useReducedMotion` check | Rule 3 | Use `useSafeMotion` hook |
+| `animate={{ width: "100%" }}` | Rule 4 | Use `scaleX` transform instead |
+| `transition={{ duration: 0.4 }}` inline | Rule 5 | Use `motionTokens.duration.normal` |
+| `{ stiffness: 300, damping: 30 }` inline | Rule 6 | Use `springs.snappy` |
+| Missing `"use client"` directive | Rule 7 | Add to top of file |
+| `navigator.hardwareConcurrency` at module level | Rule 8 | Wrap in `typeof navigator !== "undefined"` |
+
+## Related Skills
+
+- **`motion-patterns`** — consumes tokens and springs defined here to build button, modal, stagger, page transition, and scroll patterns. Does not redefine any values.
+- **`motion-advanced`** — consumes tokens and springs defined here for drag, SVG, text, and gesture patterns. Adds `useAnimate` sequences and custom hooks on top of this foundation.
diff --git a/skills/motion-patterns/SKILL.md b/skills/motion-patterns/SKILL.md
new file mode 100644
index 00000000..80ff2dd9
--- /dev/null
+++ b/skills/motion-patterns/SKILL.md
@@ -0,0 +1,419 @@
+---
+name: motion-patterns
+description: Production-ready animation patterns for React / Next.js — button, modal, toast, stagger, page transitions, exit animations, scroll, and layout — built on motion-foundations tokens and springs.
+version: 1.0
+tags: [motion, animation, ui-patterns]
+category: frontend
+author: jeff
+---
+
+# Motion Patterns
+
+Copy-paste patterns for the most common UI animation needs.
+Every pattern here is built on `motion-foundations` tokens and springs.
+Do not define new duration or easing values here — import them.
+
+## When to Activate
+
+- Animating a button, card, modal, or toast notification
+- Building list entrances with stagger
+- Setting up page transitions in Next.js App Router
+- Adding entrance or exit animations to conditional content
+- Implementing scroll-reveal, scroll-linked progress, or sticky story sections
+- Building expanding cards, accordions, or shared-element transitions
+
+## Outputs
+
+This skill produces:
+
+- Accessible, SSR-safe animation for all standard UI components
+- `AnimatePresence`-wrapped conditional renders with correct exit behavior
+- Page transition wrapper component for Next.js App Router
+- Scroll-reveal and scroll-linked patterns using `useScroll` + `useTransform`
+- Layout animation patterns (`layout`, `layoutId`) for expanding and crossfading elements
+
+## Principles
+
+- Every pattern imports from `motion-foundations`. No raw numbers.
+- Every conditional render is wrapped in `AnimatePresence` with a `key`.
+- Exit animations are always defined alongside enter animations — never as an afterthought.
+- `layout` is used only for small, isolated shifts. Large subtrees get explicit transforms.
+
+## Rules
+
+1. **Always wrap conditional renders in `AnimatePresence` with a `key`** on the direct child. Without a key, exit animations never fire.
+2. **Always define `exit` when defining `initial` + `animate`.** An animation without an exit is incomplete.
+3. **Use `mode="wait"` on page transitions.** Enter must not start until exit completes.
+4. **Never use `layout` on subtrees with more than ~5 children or deeply nested DOM.** Use explicit `x`/`y` transforms instead.
+5. **Stagger interval must stay between `0.05s` and `0.10s`.** Below feels mechanical; above feels sluggish.
+6. **Modals must always include:** focus trap, Escape-key close, scroll lock, `role="dialog"`, `aria-modal="true"`.
+7. **Scroll reveals use `viewport={{ once: true }}`.** Repeating on scroll-out is distracting, not informative.
+8. **All token values are imported from `motion-foundations`.** No inline numbers.
+
+## Decision Guidance
+
+### Choosing the right pattern
+
+| Situation | Pattern |
+| ---------------------------------------- | ---------------------- |
+| Element appears / disappears | `AnimatePresence` |
+| List of items loading in sequence | Stagger variants |
+| Navigating between routes | Page transition wrapper|
+| Element changes size in place | `layout` prop |
+| Same element moves across page contexts | `layoutId` |
+| Element enters when scrolled into view | `whileInView` |
+| Value tied to scroll position | `useScroll` + `useTransform` |
+
+### When to use `mode="wait"` vs `mode="sync"`
+
+| Mode | Use when |
+| ------- | --------------------------------------- |
+| `wait` | Page transitions, content swaps (one at a time) |
+| `sync` | Stacked notifications, list items (overlap is fine) |
+| `popLayout` | Items removed from a reflow list |
+
+## Core Concepts
+
+### AnimatePresence contract
+
+Three things must always be true:
+
+1. `AnimatePresence` wraps the conditional
+2. The direct child has a `key`
+3. The child has an `exit` prop
+
+Miss any one of these and the exit animation silently fails.
+
+### layout vs layoutId
+
+- `layout` — animates the element's own size/position change in place
+- `layoutId` — links two separate elements, crossfading between them across renders
+
+Use `layout="position"` on text inside an expanding container to prevent text reflow from animating.
+
+## Code Examples
+
+### Button feedback
+
+```tsx
+"use client"
+import { motion } from "motion/react"
+import { springs, motionTokens } from "@/lib/motion-tokens"
+
+
+```
+
+### Stagger list
+
+```tsx
+"use client"
+import { motion } from "motion/react"
+import { motionTokens, springs } from "@/lib/motion-tokens"
+
+const container = {
+ hidden: {},
+ visible: {
+ transition: {
+ staggerChildren: 0.08, // within the 0.05–0.10 rule
+ delayChildren: 0.1,
+ },
+ },
+}
+
+const item = {
+ hidden: { opacity: 0, y: motionTokens.distance.md },
+ visible: { opacity: 1, y: 0, transition: springs.gentle },
+}
+
+
+ {items.map((i) => (
+
+ ))}
+
+```
+
+### Modal
+
+```tsx
+"use client"
+import { motion, AnimatePresence } from "motion/react"
+import { springs } from "@/lib/motion-tokens"
+
+// Wrap at the call site:
+// {isOpen && }
+
+export function Modal({ onClose }: { onClose: () => void }) {
+ return (
+ <>
+ {/* Overlay */}
+
+
+ {/* Panel — accessibility requirements: focus trap, Escape close,
+ scroll lock, role="dialog", aria-modal="true" */}
+
+ >
+ )
+}
+```
+
+### Toast stack
+
+```tsx
+"use client"
+import { motion, AnimatePresence } from "motion/react"
+import { springs } from "@/lib/motion-tokens"
+
+
+ {toasts.map((t) => (
+
+ ))}
+
+```
+
+### Page transition (Next.js App Router)
+
+```tsx
+// components/page-transition.tsx
+"use client"
+import { motion, AnimatePresence } from "motion/react"
+import { usePathname } from "next/navigation"
+import { motionTokens } from "@/lib/motion-tokens"
+
+const variants = {
+ initial: { opacity: 0, y: motionTokens.distance.sm },
+ enter: { opacity: 1, y: 0 },
+ exit: { opacity: 0, y: -motionTokens.distance.sm },
+}
+
+export function PageTransition({ children }: { children: React.ReactNode }) {
+ const pathname = usePathname()
+ return (
+
+
+ {children}
+
+
+ )
+}
+```
+
+### Scroll reveal
+
+```tsx
+"use client"
+import { motion } from "motion/react"
+import { motionTokens, springs } from "@/lib/motion-tokens"
+
+
+```
+
+### Scroll progress bar
+
+```tsx
+"use client"
+import { motion, useScroll } from "motion/react"
+
+export function ScrollProgress() {
+ const { scrollYProgress } = useScroll()
+ return (
+
+ )
+}
+```
+
+### Expanding card
+
+```tsx
+"use client"
+import { useState } from "react"
+import { motion, AnimatePresence } from "motion/react"
+import { springs, motionTokens } from "@/lib/motion-tokens"
+
+export function ExpandingCard({ title, body }: { title: string; body: string }) {
+ const [expanded, setExpanded] = useState(false)
+
+ return (
+ setExpanded(!expanded)} className="cursor-pointer">
+ {/* layout="position" prevents text reflow from animating */}
+
+ {title}
+
+
+
+ {expanded && (
+
+ {body}
+
+ )}
+
+
+ )
+}
+```
+
+### Shared-element crossfade
+
+```tsx
+// Source context
+
+
+// Destination context (same layoutId — motion handles the transition)
+
+```
+
+### Accordion
+
+```tsx
+
+ {children}
+
+```
+
+## End-to-End Example
+
+A staggered list that enters on mount, handles conditional presence, and
+respects reduced motion — combining tokens, springs, AnimatePresence, and
+the accessibility hook from `motion-foundations`:
+
+```tsx
+"use client"
+import { useState } from "react"
+import { motion, AnimatePresence } from "motion/react"
+import { motionTokens, springs } from "@/lib/motion-tokens"
+import { useSafeMotion } from "@/hooks/use-reduced-motion"
+
+const containerVariants = {
+ hidden: {},
+ visible: {
+ transition: { staggerChildren: 0.08, delayChildren: 0.1 },
+ },
+}
+
+function ListItem({ label, onRemove }: { label: string; onRemove: () => void }) {
+ const safe = useSafeMotion(motionTokens.distance.sm)
+ return (
+
+ {label}
+
+
+ )
+}
+
+export function AnimatedList({ items, onRemove }: {
+ items: { id: string; label: string }[]
+ onRemove: (id: string) => void
+}) {
+ return (
+
+
+ {items.map((item) => (
+ onRemove(item.id)}
+ />
+ ))}
+
+
+ )
+}
+```
+
+## Constraints / Non-Goals
+
+This skill does **not** cover:
+
+- Token and spring definitions → see `motion-foundations`
+- Drag interactions, swipe gestures, reorderable lists → see `motion-advanced`
+- Text animations (word/character reveal, counters) → see `motion-advanced`
+- SVG path drawing or morphing → see `motion-advanced`
+- Custom animation hooks → see `motion-advanced`
+- CSS-only transitions not using `motion/react`
+
+## Anti-Patterns
+
+| Anti-pattern | Rule violated | Fix |
+| -------------------------------------------- | ------- | ------------------------------------------ |
+| `AnimatePresence` child missing `key` | Rule 1 | Add stable `key` to the direct child |
+| `initial` + `animate` without `exit` | Rule 2 | Always define all three together |
+| Page transition without `mode="wait"` | Rule 3 | Add `mode="wait"` to `AnimatePresence` |
+| `layout` on a 50-item list | Rule 4 | Use `mode="popLayout"` or explicit transforms |
+| `staggerChildren: 0.2` on a 10-item list | Rule 5 | Cap at `0.08–0.10` |
+| Modal without focus trap | Rule 6 | Add `focus-trap-react` or Radix Dialog |
+| `whileInView` without `viewport={{ once: true }}` | Rule 7 | Repeating entrances distract, not inform |
+| `transition={{ duration: 0.3 }}` inline | Rule 8 | Use `motionTokens.duration.normal` |
+
+## Related Skills
+
+- **`motion-foundations`** — defines all tokens, springs, the `useSafeMotion` hook, and SSR guards that every pattern here imports. Must be set up first.
+- **`motion-advanced`** — extends these patterns with drag, gestures, SVG, text, custom hooks, and imperative sequencing. Does not redefine any patterns from this skill.