# Motion

Every Eased component moves with the same small vocabulary: three springs, four curves and five durations. Each one exists twice, as a CSS variable in the stylesheet and as a Motion object in `components/eased/lib/motion.ts`. Both are sampled from the same physics, so a CSS transition and a Motion animation with the same name feel identical.

## Springs

| Spring | Motion | CSS | Use it for |
|---|---|---|---|
| `snappy` | `{ type: "spring", stiffness: 520, damping: 38 }` | `ease-spring-snappy`, 350ms | Presses, toggles, thumbs, indicators, text swaps, rolling digits and the focus ring |
| `smooth` | `{ type: "spring", visualDuration: 0.4, bounce: 0 }` | `ease-spring-smooth`, 450ms | Height and layout changes, panels, and toast and tooltip position. No overshoot. |
| `bouncy` | `{ type: "spring", stiffness: 400, damping: 26 }` | `ease-spring-bouncy`, 600ms | Shape morphs and playful reveals only. Never on an exit. |

### Use a spring in CSS

```css
.thumb {
  transition: translate var(--eased-duration-spring-snappy) var(--eased-spring-snappy);
}
```

In CSS, the springs are `linear()` curves, so they work in a plain transition. Pair each spring with its duration variable.

### Use a spring in Motion

```tsx
import { motion } from "motion/react";
import { snappy } from "@/components/eased/lib/motion";

<motion.span layoutId="indicator" transition={snappy} />
```

## Curves and durations

| Name | Value | Use it for |
|---|---|---|
| `ease-enter` | `cubic-bezier(0.16, 1, 0.3, 1)` | Things that appear |
| `ease-standard` | `cubic-bezier(0.22, 1, 0.36, 1)` | Colour and opacity while something stays on screen |
| `ease-exit` | `cubic-bezier(0.7, 0, 0.84, 0)` | Things that leave |
| `--eased-ease-in-out` | `cubic-bezier(0.65, 0, 0.35, 1)` | Movement that starts and ends on screen |
| `--eased-duration-instant` | 100ms | Press-down and the reduced-motion fade |
| `--eased-duration-fast` | 150ms | Hover and colour changes |
| `--eased-duration-standard` | 200ms | Popovers and tooltips |
| `--eased-duration-slow` | 300ms | Dialogs |
| `--eased-duration-sheet` | 500ms | Sheets and drawers |

Interaction feedback never takes longer than 200ms. The enter curve, `cubic-bezier(0.16, 1, 0.3, 1)`, is the curve in the Eased logo.

## Enter and exit

```tsx
import { enter, exit } from "@/components/eased/lib/motion";

<motion.div
  initial={{ opacity: 0, y: 6, scale: 0.97 }}
  animate={{ opacity: 1, y: 0, scale: 1, transition: enter }}
  exit={{ opacity: 0, y: 3, scale: 0.98, transition: exit }}
/>
```

Use the `enter` and `exit` pairs from `motion.ts`. An element that appears is something to look at; an element that leaves is in the way. So exits:

- take at most three quarters of the enter time
- use an ease-in curve
- never overshoot

Anything that opens from a trigger grows from it. Set its `transform-origin` to the trigger, and enter from 6px toward it at 97% scale. Nothing scales up from zero.

## Swap a label in place

```tsx
import { AnimatePresence, motion } from "motion/react";
import { swap, swapIn, swapOut, swapShown } from "@/components/eased/lib/motion";

<AnimatePresence mode="popLayout" initial={false}>
  <motion.span key={label} initial={swapIn} animate={swapShown} exit={swapOut} transition={swap}>
    {label}
  </motion.span>
</AnimatePresence>
```

Use the swap when a label or icon changes in place, such as Save to Saved, or a copy icon to a check. The old text moves up 3px and the new text rises from 4px below, each with a soft blur, so the eye reads one change instead of a flicker. The blur is tweened separately from the spring, so it clears before the movement settles. Text Swap and Button use this pattern.

## Press

| Class in `press.ts` | Scale | Use it for |
|---|---|---|
| `pressIcon` | 0.96 | Icon buttons up to 48px |
| `press` | 0.97 | Text buttons |
| `pressWide` | 0.985 | Bars and cards wider than 220px |

Buttons press down in 100ms and spring back on `snappy`. The scale depends on width, so a wide bar doesn't dip further than a small icon. Triggers that open a popover or menu never scale; they press by colour instead.

## Stagger

| `stagger` key | Step |
|---|---|
| `char` | 16ms per character |
| `item` | 35ms per item |
| `word` | 40ms per word |
| `line` | 80ms per line |

Lists that enter together step through `stagger` in `motion.ts`. Keep the whole stagger under 400ms. It runs once when the list mounts, never on a re-render.

## Reduced motion

```tsx
import { reducedFade, snappy, useSafeReducedMotion } from "@/components/eased/lib/motion";

const reduced = useSafeReducedMotion();
<motion.div layout transition={reduced ? reducedFade : snappy} />
```

Every animated component has a reduced branch. When the visitor's system asks for reduced motion, movement becomes a 100ms opacity fade (`reducedFade`) or nothing, and looping animations stop.

Read the setting with `useSafeReducedMotion()` from `motion.ts`, not Motion's `useReducedMotion`. It returns `false` on the server and during hydration, then the real value, so reduced-motion visitors don't get a hydration mismatch.

In CSS, the `reduced:` variant applies under `prefers-reduced-motion: reduce`, for example `reduced:transition-none`.

## What never animates

- Animate only `transform`, `opacity`, `clip-path`, `filter` and colours. Never animate `width`, `height`, `top`, `left`, `margin`, `padding` or `box-shadow` frame by frame, and never use `transition: all`.
- Don't animate keyboard-driven, high-frequency actions: opening a command menu, adding or removing list items, opening a context menu or typing.
- Hover changes only colour or shadow, by one step. It never changes size, weight or position.
- Looping motion pauses off-screen and under reduced motion.
- Set `will-change` only during a gesture, and remove it after.
