# MOTION.md — animation catalog ## Easings (canonical) ```css --ease-out: cubic-bezier(0.22, 1, 0.36, 1); --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); ``` JS form (motion/react `ease` prop): `[0.22, 1, 0.36, 1]` for entries, `[0.65, 0, 0.35, 1]` for movement. Rules: - Entries / exits → `ease-out`. - Element moving on screen → `ease-in-out`. - Continuous (marquee, progress, count-up) → `linear`. - Decorative pulse → `easeInOut` string OK. - **Never** `ease-in` for UI. **Never** bounce/elastic. ## Durations (canonical) ```css --dur-micro: 150ms; /* button press, hover swap */ --dur-short: 300ms; /* hover, focus rings */ --dur-med: 500ms; /* dropdowns, popovers */ --dur-long: 800ms; /* slide-level reveals */ ``` Cinematic slide entries may exceed 300 ms (450–900 ms is normal for `Reveal`). UI microinteractions stay < 300 ms. ## Stagger - Word/line stagger: 60–120 ms. - Item stagger in lists: 30–100 ms. - Slide reveal cascade: 150 ms between elements feels editorial; 60–80 ms feels snappy. ## Primitive patterns ### Reveal (fade + translate + blur) ```ts hidden: { opacity: 0, x, y, filter: "blur(8px)" } show: { opacity: 1, x: 0, y: 0, filter: "blur(0)" } transition: { duration: 0.7, delay, ease: [0.22, 1, 0.36, 1] } viewport: { once: true, amount: 0.3 } ``` Direction offsets: up `y:40`, down `y:-40`, left `x:40`, right `x:-40`. `distance` prop overrides magnitude. ### StaggerList (cascading list items) Parent `staggerChildren`, child `opacity + y(24) + blur(6)` → final. Default stagger 0.1 s, duration 0.6 s. ### MaskReveal (line-by-line hero text) Each line wrapped in `overflow-hidden` span. Inner translate from `y:115%` to `y:0`. Stagger 0.12 s, duration 0.85 s. Use for headlines that need physical weight. ### AnimatedText (word/char split) Splits text into words (default) or chars. Each token has its own `overflow-hidden` mask. Stagger 0.06 s per word, duration 0.85 s. Useful for hero phrases where you want individual word entries. ### Highlight (yellow fill animation) `backgroundColor: rgba(250,204,13,0) → 1` with `color: accent-500 → ink-900` over 0.5 s, ease-out, when in viewport. Triggers once. Use to "land" a key word. ### Counter `useMotionValue(from) → animate(to)`, `useInView` triggers. Default duration 0.8 s, delay 0.2 s, easing `linear` (system convention for count-up; values "tick" naturally). ### LetterCounter Maps `AA` ↔ `NN` etc. via base-26. Duration 1.0 s, linear. Used for placeholder cifras that should "spin" before revealing actual data. ### Spotlight Cursor-following radial gradient inside a slide. `useSpring` with `stiffness:100, damping:24, mass:0.6`. Hover-device only. Position 0–100% via mouse coords. ### HeroCanvas (constellation) Canvas with ~50–120 drifting particles, link lines under `LINK_DISTANCE=140` px, pointer influence radius 220 px. Particles tint to accent within pointer radius. Friction 0.985 per frame. Reduces particles when motion is reduced. ### SmoothScroll (Lenis) ```ts new Lenis({ duration: 1.1, easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)), smoothWheel: true, wheelMultiplier: 1, touchMultiplier: 1.5, }); ``` Mount once in root layout. Hooks into `requestAnimationFrame`. ## Slide chrome motion - **Top progress bar**: `useScroll().scrollYProgress` → `useSpring(stiffness:120, damping:30, mass:0.4)` → `scaleX` on a fixed div. - **Slide indicator dots**: active state change with `transition-all duration-300`. (Note: this is the one explicit `transition: all` in the system; acceptable because of micro element. Avoid pattern elsewhere.) - **Active block eyebrow** (top-left fixed): color flip when active slide is `feature` variant. ## Keyboard ``` ArrowDown / ArrowRight / PageDown / Space → next slide ArrowUp / ArrowLeft / PageUp → prev slide Home → first End → last F → toggle fullscreen ``` `scrollIntoView({ behavior: "smooth", block: "start" })`. Lenis handles the easing. ## prefers-reduced-motion Global CSS: ```css @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; transition-duration: 0.001ms !important; scroll-behavior: auto !important; } } ``` Each motion primitive also checks `useReducedMotion()` and degrades to opacity-only or no transition. ## Hover devices only Spotlight, HeroCanvas pointer effects, magnetic interactions: gate with ```ts window.matchMedia("(hover: hover)").matches ``` and the CSS ```css @media (hover: hover) and (pointer: fine) { ... } ```