# Motion tokens and presets

> Five sampled springs, one attribute to switch the whole page, and the durations, travel and stagger every component resolves from, as CSS custom properties and an opt-in Tailwind v4 layer.

No component hard-codes a curve, a duration, a travel distance or a stagger.
Everything resolves from six tokens on `:root`, and those six are filled by
the active **preset**:

| Preset | Spring (stiffness / damping) | Settles in | Feel |
|---|---|---:|---|
| `snap` | 1218 / 70 | 290 ms | clicks into place |
| `ui` (default) | 305 / 33 | 450 ms | product UI |
| `gentle` | 110 / 20 | 760 ms | editorial |
| `lively` | 622 / 17 | 840 ms | overshoots, bounces back |
| `ambient` | 43 / 13 | 1390 ms | slow, atmospheric |

Each spring is a real mass–spring–damper simulation sampled into a CSS
`linear()` function, with the time it takes to settle within 0.1% as the
duration. That is a spring without a physics runtime: the curve is a
timing function, the browser interpolates it, and it costs nothing per frame.

## Switch the page

```html
<html data-ma-preset="lively">
```

Any subtree can carry its own: `<section data-ma-preset="snap">`. The
attribute sets these six tokens; components read them:

```css
--ma-ease            /* the sampled spring */
--ma-duration        /* its settle time */
--ma-stagger         /* 40 / 60 / 120 ms */
--ma-travel-enter    /* 16–48 px, how far an entrance travels */
--ma-travel-hover    /* 2–6 px */
--ma-travel-section  /* 48 px, page-level motion */
```

The homepage's preset switcher is five radio inputs and a `:root:has()` rule
per preset: the same tokens, switched with zero JavaScript.

## Fixed curves

Exits and continuous motion never use a spring:

| Token | Value | Use |
|---|---|---|
| `--ma-ease-out` | `cubic-bezier(0.23, 1, 0.32, 1)` | exits, hover release |
| `--ma-ease-in` | `cubic-bezier(0.5, 0, 0.75, 0)` | things leaving |
| `--ma-ease-in-out` | `cubic-bezier(0.77, 0, 0.175, 1)` | ambient loops |
| `--ma-ease-drawer` | `cubic-bezier(0.32, 0.72, 0, 1)` | sheets |
| `--ma-duration-fast` | `150ms` | exits, hover, press |
| `--ma-duration-slow` | `700ms` | long reveals |

Override any of them in your own CSS: `:root { --ma-ease: … }`. The base
stylesheet has zero specificity, so yours wins. A prop on a component
(`<Reveal duration={900}>`) sets a component-local property that wins over
the preset for that instance only.

## Tailwind v4

```css
@import 'tailwindcss';
@import '@moonarc/core/styles';
```

That adds `ease-ma` / `duration-ma` (the active preset), `ease-snap` …
`ease-ambient` and `duration-snap` … `duration-ambient` (the presets by name),
plus `ease-out-quint`, `ease-in-quint`, `ease-in-out-quint`, `ease-drawer`,
`duration-fast`, `duration-base`, `duration-slow` as real utilities mapped onto
the same custom properties.

Tailwind is never required. Components ship their own layered CSS; this file is
the only Tailwind-specific thing in the package, and it is opt-in.

## Cascade layers

The base stylesheet declares `@layer theme, base, components, utilities` and
puts component rules in `components`. Tailwind v4 puts utilities in
`utilities`, so `class="p-0"` on a component always wins. Without Tailwind,
any unlayered rule of yours beats the library. Either way you never fight
specificity.

The flip side: a reset of your own that is unlayered beats the library too. A
hand-written `a { color: inherit }` outside any layer recolours every link a
component draws, including a primary button's label, which then sits on the
primary colour in your body text colour. Put resets in `@layer base`, where
Tailwind's preflight already is.