diff --git a/skills/motion_ui/skill.md b/skills/motion_ui/skill.md
index e7fe30b6..a5462dd9 100644
--- a/skills/motion_ui/skill.md
+++ b/skills/motion_ui/skill.md
@@ -1,10 +1,9 @@
-````md
---
name: motion-ui
description: "Production-ready UI motion system for React/Next.js. Use when implementing animations, transitions, or motion patterns."
---
-# Motion System v4.1
+# Motion System v4.2
Production-ready UI motion system for React / Next.js.
@@ -12,75 +11,98 @@ Focused on **performance, accessibility, and usability** — not decoration.
**by Jatan**
----
-
## When to Use
Use this motion system when motion:
-- Guides attention (onboarding, primary actions)
-- Communicates state (loading, success, error, transitions)
-- Preserves spatial continuity (navigation, layout changes)
+* Guides attention (e.g., onboarding, key actions)
+* Communicates state (loading, success, error, transitions)
+* Preserves spatial continuity (layout changes, navigation)
### Appropriate Scenarios
-- Interactive UI (buttons, modals, menus)
-- State transitions (open/close, loading states)
-- Navigation transitions and shared elements
+* Interactive components (buttons, modals, menus)
+* State transitions (loading → loaded, open → closed)
+* Navigation and layout continuity (shared elements, crossfade)
### Considerations
-- Accessibility must be preserved (reduced motion support)
-- Low-end device performance must be respected
-- Prefer responsiveness over visual smoothness
+* **Accessibility**: Always support reduced motion
+* **Device adaptation**: Adjust for low-end devices
+* **Performance trade-offs**: Prefer responsiveness over visual smoothness
-### Avoid Motion When
+### Avoid Using Motion When
-- It is purely decorative
-- It reduces clarity or usability
-- It impacts performance
+* It is purely decorative
+* It reduces usability or clarity
+* It impacts performance negatively
---
-## Core Principle
+## How It Works
+
+### Core Principle
Motion must:
-- Guide attention
-- Communicate state
-- Preserve spatial continuity
+* Guide attention
+* Communicate state
+* Preserve spatial continuity
If it does none → remove it.
---
-## Installation
+### Installation
```bash
npm install motion
-````
+```
---
-## Versions
+### Version
-* `motion/react` → default
-* `framer-motion` → legacy (do not mix)
+* `motion/react` → default (v11+, package: `motion`)
+* `framer-motion` → legacy (v10 and below, package: `framer-motion`)
----
+**Do not mix.** Mixing causes conflicting internal schedulers and broken `AnimatePresence` contexts — components from one package will not coordinate exit animations with components from the other.
-## Motion Tokens
+To check which version your project uses:
+
+```bash
+cat package.json | grep -E '"motion"|"framer-motion"'
+```
+
+Always import from one source consistently:
```ts
+// ✅ Correct (modern)
+import { motion, AnimatePresence } from "motion/react"
+
+// ✅ Correct (legacy)
+import { motion, AnimatePresence } from "framer-motion"
+
+// ❌ Never mix both in the same project
+```
+
+---
+
+### Motion Tokens
+
+```ts
+// motionTokens.ts
export const motionTokens = {
duration: {
fast: 0.18,
normal: 0.35,
slow: 0.6
},
+ // Use these as the `ease` value inside a `transition` object:
+ // transition={{ duration: motionTokens.duration.normal, ease: motionTokens.easing.smooth }}
easing: {
- smooth: [0.22, 1, 0.36, 1],
- sharp: [0.4, 0, 0.2, 1]
+ smooth: [0.22, 1, 0.36, 1] as [number, number, number, number],
+ sharp: [0.4, 0, 0.2, 1] as [number, number, number, number]
},
distance: {
sm: 8,
@@ -90,48 +112,67 @@ export const motionTokens = {
}
```
+Usage example:
+
+```tsx
+import { motionTokens } from "@/lib/motionTokens"
+
+
+```
+
---
-## Performance Rules
+### Performance Rules
-### Safe Properties
+**Safe**
* transform
* opacity
-### Avoid
+**Avoid**
-* width
-* height
-* top
-* left
+* width / height
+* top / left
Rule: responsiveness > smoothness
---
-## Device Adaptation
+### Device Adaptation
+
+The heuristic combines CPU core count **and** available memory for a more reliable signal. `deviceMemory` is available on Chrome/Android; the fallback covers Safari and Firefox.
```ts
const isLowEnd =
- typeof navigator !== "undefined" &&
- navigator.hardwareConcurrency <= 4
+ typeof navigator !== "undefined" && (
+ // Low memory (Chrome/Android only; undefined elsewhere → treat as capable)
+ (navigator.deviceMemory !== undefined && navigator.deviceMemory <= 2) ||
+ // Few cores AND no memory API (covers Safari/Firefox on weak hardware)
+ (navigator.deviceMemory === undefined && navigator.hardwareConcurrency <= 4)
+ )
const duration = isLowEnd ? 0.2 : 0.4
```
---
-## Accessibility
+### Accessibility
-### Reduced Motion (React)
+#### JS (useReducedMotion)
```tsx
import { motion, useReducedMotion } from "motion/react"
-const reduce = useReducedMotion()
+export function FadeIn() {
+ const reduce = useReducedMotion()
-export function Example() {
return (
@@ -163,79 +204,102 @@ export function Example() {
---
-## Core Patterns
+### Architecture & Patterns
-* hover → whileHover
-* tap → whileTap
-* in-view → whileInView
-* scroll → useScroll
-* conditional → AnimatePresence
-* small layout → layout
-* large layout → avoid
-* complex → useAnimate
+#### Core Patterns
----
+| Scenario | Pattern |
+|---|---|
+| Hover feedback | `whileHover` |
+| Tap / press feedback | `whileTap` |
+| Reveal on scroll | `whileInView` |
+| Scroll-linked value | `useScroll` + `useTransform` |
+| Conditional mount/unmount | `AnimatePresence` |
+| Small layout shifts (single element, < ~300px change) | `layout` prop |
+| Large layout shifts or full-page reflows | Avoid `layout`; use CSS transitions or page-level routing instead |
+| Complex, imperative sequences | `useAnimate` |
-## Layout System
+> **Why avoid `layout` on large containers?** Framer's layout animation uses `transform` to reconcile positions, but on elements that span the full viewport or trigger deep reflow, the measurement cost causes visible jank and CLS. Prefer CSS Grid/Flexbox transitions or coordinate with `layoutId` on specific child elements only.
-* layoutId → shared transitions
-* AnimatePresence → mount/unmount transitions
+#### Layout & Transitions
----
+* Shared element transitions → `layoutId` (must be unique per mounted instance)
+* Enter / exit transitions → `AnimatePresence` (see `mode` guidance below)
-## Advanced Patterns
+#### AnimatePresence `mode`
-* Parallax scrolling
-* Scroll storytelling sections
-* 3D pointer tilt
-* Crossfade transitions
-* Clip-path reveals
-* Skeleton loading loops
-* Micro-interactions
-* Spring physics motion
+Always specify `mode` explicitly — the default (`"sync"`) runs enter and exit simultaneously, which causes visual overlap in most UI patterns.
----
-
-## Modal System (Production Safe)
+| `mode` | When to use |
+|---|---|
+| `"wait"` | Exit completes before enter starts. Use for **modals, toasts, page transitions**. |
+| `"sync"` (default) | Enter and exit overlap. Use only when overlap is intentional (e.g., crossfade carousels). |
+| `"popLayout"` | Exiting element is popped out of flow immediately; remaining items animate to fill. Use for **lists, tabs, dismissible cards**. |
```tsx
-import { useEffect, useRef, useState } from "react"
+// Modal — always use "wait"
+
+ {open && }
+
+
+// Dismissible list item — use "popLayout"
+
+ {items.map(item => )}
+
+```
+
+---
+
+### Advanced Patterns (Concepts)
+
+* Parallax (scroll-linked transforms)
+* Scroll storytelling (sticky sections)
+* 3D tilt (pointer-based transforms)
+* Crossfade (shared `layoutId`)
+* Progressive reveal (clip-path)
+* Skeleton loading (looped opacity)
+* Micro-interactions (hover/tap feedback)
+* Spring system (physics-based motion)
+
+---
+
+### Modal Essentials
+
+* Focus trap
+* Escape close
+* Scroll lock
+* ARIA roles
+* Use `AnimatePresence mode="wait"` so exit animation completes before the next modal enters
+
+#### Full Example
+
+```tsx
+import React, { useEffect, useRef, useState } from "react"
import { motion, AnimatePresence } from "motion/react"
-type ModalProps = {
- open: boolean
- onClose: () => void
-}
-
-function useFocusTrap(ref: React.RefObject, active: boolean) {
+function useFocusTrap(ref: React.RefObject, active: boolean) {
useEffect(() => {
if (!active || !ref.current) return
-
const el = ref.current
-
const focusable = el.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
)
-
const first = focusable[0]
- const last = focusable[focusable.length - 1]
+ const last = focusable[focusable.length - 1]
- if (first) first.focus()
-
- const handleKey = (e: KeyboardEvent) => {
+ function handleKey(e: KeyboardEvent) {
if (e.key !== "Tab") return
- if (!first || !last) return
-
if (e.shiftKey && document.activeElement === first) {
e.preventDefault()
- last.focus()
+ last?.focus()
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault()
- first.focus()
+ first?.focus()
}
}
el.addEventListener("keydown", handleKey)
+ first?.focus()
return () => el.removeEventListener("keydown", handleKey)
}, [active, ref])
}
@@ -243,63 +307,223 @@ function useFocusTrap(ref: React.RefObject, active: boolean) {
function useScrollLock(active: boolean) {
useEffect(() => {
if (!active) return
-
const prev = document.body.style.overflow
document.body.style.overflow = "hidden"
-
- return () => {
- document.body.style.overflow = prev
- }
+ return () => { document.body.style.overflow = prev }
}, [active])
}
-export function Modal({ open, onClose }: ModalProps) {
+function Modal({ open, closeModal }: { open: boolean; closeModal: () => void }) {
const ref = useRef(null)
useFocusTrap(ref, open)
useScrollLock(open)
useEffect(() => {
- const onKeyDown = (e: KeyboardEvent) => {
- if (e.key === "Escape") onClose()
+ function onKey(e: KeyboardEvent) {
+ if (e.key === "Escape") closeModal()
}
-
- if (open) window.addEventListener("keydown", onKeyDown)
-
- return () => window.removeEventListener("keydown", onKeyDown)
- }, [open, onClose])
+ if (open) window.addEventListener("keydown", onKey)
+ return () => window.removeEventListener("keydown", onKey)
+ }, [open, closeModal])
return (
-
+ // mode="wait" ensures exit animation finishes before any new modal enters
+
{open && (
-
+
)}
)
}
+
+export function Example() {
+ const [open, setOpen] = useState(false)
+
+ return (
+ <>
+
+ setOpen(false)} />
+ >
+ )
+}
+```
+
+---
+
+### SSR Safety
+
+* Match initial states between server and client renders
+* Avoid implicit animation origins (always set `initial` explicitly)
+* Wrap motion components in `"use client"` in Next.js App Router
+
+---
+
+### Debugging
+
+Check:
+
+* Wrong import (mixing `motion/react` and `framer-motion`)
+* Missing `"use client"` directive in Next.js App Router
+* Missing `key` prop on `AnimatePresence` children
+* Hydration mismatch (initial state differs between SSR and client)
+* `layout` prop misuse on large containers causing reflow jank
+* State-driven animation not triggering (check dependency arrays)
+
+---
+
+### QA
+
+* No CLS
+* Keyboard works
+* Focus trapped in modals
+* ARIA roles correct (`role="dialog"`, `aria-modal="true"`)
+* Reduced motion respected (`useReducedMotion` + CSS media query)
+* No hydration warnings in Next.js
+* Animations stop cleanly on unmount (no memory leaks)
+* `AnimatePresence mode` set explicitly on all usage sites
+
+---
+
+### Anti-Patterns
+
+* Animating layout properties (`width`, `height`, `top`, `left`)
+* Infinite animations without purpose (always ask: what state does this communicate?)
+* Over-staggering lists (keep `staggerChildren` ≤ 0.1s; beyond that it feels slow)
+* Ignoring reduced motion preferences
+* Using `layout` on large or full-viewport containers
+* Omitting `mode` on `AnimatePresence` (default `"sync"` causes visual overlap)
+* Using motion purely for decoration
+
+---
+
+### Philosophy
+
+Motion is interaction design.
+
+---
+
+### Final Rule
+
+> If motion does not improve UX → remove it.
+
+---
+
+## Examples
+
+### Button Interaction
+
+```tsx
+import { motion } from "motion/react"
+
+export function Button() {
+ return (
+
+ Click me
+
+ )
+}
+```
+
+---
+
+### Reduced Motion Example
+
+```tsx
+import { motion, useReducedMotion } from "motion/react"
+
+export function FadeIn() {
+ const reduce = useReducedMotion()
+
+ return (
+
+ )
+}
+```
+
+---
+
+### Stagger List
+
+```tsx
+import { motion } from "motion/react"
+
+const container = {
+ hidden: {},
+ visible: {
+ transition: { staggerChildren: 0.08 } // keep ≤ 0.1s to avoid sluggishness
+ }
+}
+
+const item = {
+ hidden: { opacity: 0, y: 10 },
+ visible: { opacity: 1, y: 0, transition: { duration: 0.3, ease: [0.22, 1, 0.36, 1] } }
+}
+
+export function List() {
+ return (
+
+ {[1, 2, 3].map(i => (
+ Item {i}
+ ))}
+
+ )
+}
+```
+
+---
+
+### Modal with AnimatePresence
+
+```tsx
+import { motion, AnimatePresence } from "motion/react"
+
+export function Modal({ open }: { open: boolean }) {
+ return (
+
+ {open && (
+
+ )}
+
+ )
+}
```
---
-## Scroll Parallax
+### Scroll Parallax
```tsx
import { useScroll, useTransform, motion } from "motion/react"
@@ -314,7 +538,7 @@ export function Parallax() {
---
-## Skeleton Loading
+### Skeleton Loading
```tsx
import { motion } from "motion/react"
@@ -322,9 +546,13 @@ import { motion } from "motion/react"
export function Skeleton() {
return (
)
}
@@ -332,81 +560,15 @@ export function Skeleton() {
---
-## Shared Layout
+### Shared Layout (Crossfade)
```tsx
import { motion } from "motion/react"
+// layoutId must be unique per mounted instance.
+// If multiple instances can exist simultaneously, append a unique id:
+// layoutId={`shared-${item.id}`}
export function Shared() {
return
}
```
-
----
-
-## Stagger List
-
-```tsx
-import { motion } from "motion/react"
-
-const container = {
- hidden: {},
- visible: {
- transition: { staggerChildren: 0.08 }
- }
-}
-
-const item = {
- hidden: { opacity: 0, y: 10 },
- visible: { opacity: 1, y: 0 }
-}
-
-export function List() {
- return (
-
- {[1, 2, 3].map(i => (
-
- Item {i}
-
- ))}
-
- )
-}
-```
-
----
-
-## Debug Checklist
-
-* correct import (`motion/react`)
-* `"use client"` in Next.js
-* no missing keys
-* no layout shift (CLS)
-* no hydration mismatch
-* reduced motion works
-* keyboard navigation works
-
----
-
-## Anti-Patterns
-
-* animating layout (width/height)
-* decorative motion
-* infinite motion without purpose
-* ignoring reduced motion
-* over-staggering lists
-
----
-
-## Philosophy
-
-Motion is interaction design.
-
----
-
-## Final Rule
-
-> If motion does not improve UX → remove it.
-
-```
-```