> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tailmotion.moumen.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# The motion model

> Seven categories, four token groups, and one override order that explains every class.

Everything in TailMotion is built from the same four groups of custom
properties. Learn the groups once and every class becomes predictable.

## The four token groups

<Steps>
  <Step title="Explicit overrides">
    `--tm-duration`, `--tm-delay`, `--tm-easing`, `--tm-iteration-count`,
    `--tm-exit-duration`.

    Deliberately **unset**, so each motion class keeps its own tuned default.
    Set them per element with `tm-duration-*`, `tm-delay-*`, `tm-ease-*`,
    `tm-repeat-*`, the Tailwind plugin, or an inline style. An explicit value
    always wins.
  </Step>

  <Step title="Profile factors">
    `--tm-duration-scale`, `--tm-emphasis`, `--tm-overshoot`.

    Inherited **multipliers**. A motion personality retunes a whole subtree by
    changing these, and each animation keeps its relative character because it
    multiplies its own default rather than being replaced by a global one.
  </Step>

  <Step title="Shared scalars">
    `--tm-distance`, `--tm-stagger-step`.

    One value each, already shared by every class that uses them, so a profile
    sets them outright rather than scaling them.
  </Step>

  <Step title="Role easings">
    `--tm-ease-entrance`, `--tm-ease-exit`, `--tm-ease-interaction`,
    `--tm-ease-morph`, `--tm-ease-emphasis`.

    Classes opt into a **role**; a profile retunes the role. Animations with a
    character of their own — linear spins, ambient drifts — stay out of this and
    keep their literal curve.
  </Step>
</Steps>

## How a class reads its duration

Every animation in the library is written this way:

```css theme={null}
animation-duration: var(--tm-duration, calc(260ms * var(--tm-duration-scale, 1)));
```

Read it right to left:

* With nothing set, the class runs at its own tuned default — 260ms here.
* An inherited profile changes `--tm-duration-scale`, so the class **scales**.
  `tm-pop` at 380ms and `tm-fade-in` at 250ms stay in proportion.
* `tm-duration-300` sets `--tm-duration` on the element, which replaces the
  whole `calc()`. Exactly 300ms, not 300ms times anything.

<Note>
  `:root` never carries a value for `--tm-duration`, `--tm-delay`, `--tm-easing`
  or `--tm-iteration-count`. A root value would shadow every per-class default in
  the library — which is exactly the bug 0.7 fixed, and which `npm run check`
  now guards against.
</Note>

## The override order

From weakest to strongest:

| # | Source                                                         | Example                                                         |
| - | -------------------------------------------------------------- | --------------------------------------------------------------- |
| 1 | The class's own tuned default, scaled by the inherited profile | `tm-slide-block-start` at 260ms × the profile factor            |
| 2 | The nearest profile scope                                      | an inner `tm-motion-expressive` beats an outer `tm-motion-calm` |
| 3 | An element-level utility                                       | `tm-duration-300`, `tm-ease-snappy`, `tm-distance-4`            |

Profiles are plain inherited custom properties, so "nearest wins" is just CSS
inheritance — there is no resolution logic to learn.

Element-level utilities beat a profile on the *same* element too, because they
are declared later in the stylesheet than the profile classes and carry
`!important` when generated by the plugin.

## Easing roles

| Role                    | Default curve                          | Used by                                                                                             |
| ----------------------- | -------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `--tm-ease-entrance`    | `cubic-bezier(0.22, 1, 0.36, 1)`       | Anything arriving: slides, fades, scale-ins, stagger, presence opening                              |
| `--tm-ease-exit`        | `cubic-bezier(0.25, 0.46, 0.45, 0.94)` | Anything leaving                                                                                    |
| `--tm-ease-interaction` | `cubic-bezier(0.25, 0.46, 0.45, 0.94)` | Press, hover, rotate-on-hover                                                                       |
| `--tm-ease-morph`       | `cubic-bezier(0.2, 0, 0, 1)`           | Something already on screen that moves or reshapes: view morph, icon swap, accordion, tab indicator |
| `--tm-ease-emphasis`    | `cubic-bezier(0.34, 1.56, 0.64, 1)`    | The only role allowed to overshoot: pop, drop, elastic, success                                     |

Entrances **and** exits both use an ease-out family. An ease-in exit delays the
visual feedback and reads as sluggish; the exit is made quieter by being shorter
and travelling less, not by starting slowly.

`tm-ease-*` on an element overrides whatever role a class uses.

## Emphasis and overshoot

Two separate factors, because "smaller" and "no bounce" are different requests.

| Factor           | Scales                                                                                                | Setting it to 0                                     |
| ---------------- | ----------------------------------------------------------------------------------------------------- | --------------------------------------------------- |
| `--tm-emphasis`  | How far a keyframe departs from rest — the 0.85 start scale of `tm-pop`, the 8px blur of `tm-blur-in` | Removes the departure entirely, leaving a pure fade |
| `--tm-overshoot` | Only the frames that travel **past** rest — the 1.05 peak of `tm-pop`, the settle of `tm-drop`        | Removes the bounce, keeps the entrance              |

```css theme={null}
/* tm-pop, in full */
0%   { scale: calc(1 - 0.15 * var(--tm-emphasis, 1)); opacity: 0; }
70%  { scale: calc(1 + 0.05 * var(--tm-overshoot, 1)); opacity: 1; }
100% { scale: 1; opacity: 1; }
```

`tm-motion-calm` sets `--tm-overshoot: 0` and `--tm-emphasis: 0.55`: the pop
still grows into place, it just stops springing.

## Which one do I want?

| If the element…                               | Reach for                                                                       |
| --------------------------------------------- | ------------------------------------------------------------------------------- |
| appears once, on mount                        | an entrance: `tm-fade-in`, `tm-slide-block-start`, `tm-pop`                     |
| toggles between open and closed               | [`tm-presence-*`](/docs/concepts/presence), or a [recipe](/docs/guides/recipes) |
| is a `[popover]`, `<dialog>` or `<details>`   | [`tm-native-*`](/docs/guides/native-elements)                                   |
| is a menu, toast, tooltip, accordion or tab   | the matching [recipe](/docs/guides/recipes)                                     |
| should reveal as the reader scrolls to it     | [`tm-scroll-*`](/docs/guides/scroll)                                            |
| is a list that should arrive in sequence      | [`tm-stagger`](/docs/concepts/choreography)                                     |
| just needs to feel calmer or livelier overall | [`tm-motion-*`](/docs/concepts/personalities) on an ancestor                    |
| loops forever in the background               | `tm-float`, `tm-drift`, `tm-pulse`, `tm-spin`                                   |

## Keyframes or transitions?

The distinction decides whether a class can be interrupted.

| Built from      | Can it reverse mid-flight?                         | Categories                                      |
| --------------- | -------------------------------------------------- | ----------------------------------------------- |
| **Keyframes**   | No. Re-triggering restarts from frame zero         | entrance, exit, continuous, stagger, scroll     |
| **Transitions** | Yes. A new target retargets from the current value | interaction, presence, recipes, native elements |

That is why presence exists as its own category rather than being a pair of
entrance and exit keyframes: a menu the user changes their mind about halfway
has to reverse from where it is, not snap back to where it started.

<Note>
  Two keyframe classes cannot be applied to the same element — whichever comes
  later in the stylesheet wins `animation-name`, not both. That is a property of
  CSS animations, not of this library. Transitions do stack, so `tm-press` and
  `tm-hover-lift` together are fine. See [Transform safety](/docs/concepts/transform-safety#stacking-two-keyframe-classes).
</Note>
