Mobile motion patterns
All animation in web/mobile uses the motion package through the shared
presets module src/lib/motion/presets.ts. Components never define their own
durations, easings, or springs.
The presets
Consume via the hook (reduced-motion aware — this is mandatory):
import {useMotionPresets} from "@/lib/motion/presets"
const {sharedAxisPush, sheetSlideUp, crossfade, reduced} = useMotionPresets()
sharedAxisPush— list → chat navigation (and any parent → child screen push). Forward usescustom={1}, back usescustom={-1}; the back gesture/button reverses the same preset. Wrap sibling screens in<AnimatePresence custom={direction} initial={false}>.sheetSlideUp— spring-based bottom sheets (project drawer). Pair with acrossfadescrim.crossfade— skeleton → content swaps. Skeleton and content must occupy identical geometry so the fade causes zero layout shift.
Rules
- Animate navigation, containment, and state swaps — not decoration. No
attention-seeking motion, no animating properties that trigger layout
(animate
transform/opacityonly). - Reduced motion is not optional.
useMotionPresets()returns instant variants whenprefers-reduced-motionis set; any animation built outside the presets module must justify itself in review AND handle reduced motion itself (prefer extending the presets module instead). - Message entrance/streaming (WP3b+): subtle and consistent with the playground's feel — entrance is a small fade/rise on the preset tokens; text streaming is never per-character animated.
- New shared patterns go INTO
presets.ts(one exported preset + doc comment), not into a component file.