The timing grammar
Two clips that each look good on their own can still look wrong end to end. A page layout is forgiving — nudge a card twenty pixels and it still reads. Time is not. If the first clip is still fading out while the second is still fading in, the join dips, and everyone watching feels it even when nobody can name it.
So every one of the 77 components here animates against the same small set of rules: one frame rate, four standard lengths, four curves, four springs, and one piece of arithmetic for overlapping two clips. That is the timing grammar, and it is the reason you can pick any two components in this library, put them back to back, and get a clean handoff without touching a keyframe.
One frame rate
Every duration in this library is written in frames, and a frame count only names a length once the frame rate is fixed. Twelve frames is 0.4s at 30fps and 0.5s at 24. Mix rates in one timeline and every budget below quietly becomes a different length than it says it is: that same 12-frame entrance runs a tenth of a second long, which is exactly enough to miss the beat it was cut to.
The canonical rate is 30fps and all 77 components declare it, at every aspect ratio they ship in — 16:9, 9:16 and 1:1. It also keeps the arithmetic legible: a two-second beat is 60 frames, a five-second scene is 150, and you can do that division in your head while writing a shot list.
Enter, hold, exit — and what may move in each
A component does not declare a duration. It declares three: the frames it takes to arrive, the frames it stays, and the frames it takes to leave. The three windows are not descriptive — they are a contract about what the component is allowed to animate, and it is the contract, not the numbers, that makes overlapping safe.
- enter
- The component brings itself on: opacity, scale, position, blur, a mask travelling across a wordmark — whatever arriving means for it. It has to be finished by the last frame of the window. Nothing may still be on its way in afterwards.
- hold
- The component may not touch its own presence. No continuing fade-in, no early fade-out, no drifting into place. Internal motion is free and in fact expected — a counter ticking, a bar growing, light moving across the ground, a slow push on the camera. A hold that is pixel-identical frame to frame reads as a dead frame, and the craft audit fails it. The rule is not nothing moves; it is the component is at full strength on every frame of the hold.
- exit
- The component takes itself off, and does nothing else. No last gesture, no final reveal — anything a viewer needs to read has already been read by now.
In code that contract is one number. The shared stage function computes presence = enter × (1 − exit) and every component multiplies its own opacity and entrance transforms by it. Two components whose presence curves cross therefore sum to a coherent cross-fade instead of a flicker: the shape of the fade is the same function on both sides of the join, rather than two hand-drawn ones that happen to be in the same place.
Lists, grids and per-glyph animation do not get budgets of their own. Each item’s enter is pushed back instead — 3 frames per item by default, 100ms at 30fps — while the component’s declared length stays what it was. The readable range is roughly 60 to 140ms between items; past that a list stops reading as one gesture and starts reading as separate events, and the tail of a long one is still arriving after the entrance should have been over.
Four budgets
Rather than let every component invent its own three numbers, there are four standard sets. Picking one is the first decision an author or an agent makes, and it is really a decision about how long a viewer needs to read the frame.
| Budget | Enter | Hold | Exit | Total | Seconds | In use |
|---|---|---|---|---|---|---|
| beatLower thirds, badges, captions. Something you glance at. | 12f | 36f | 12f | 60f | 2.0s | 2 |
| statementTitle cards, kinetic headlines. One line you actually read. | 15f | 60f | 15f | 90f | 3.0s | 9 |
| sceneProduct shots, charts — anything read rather than glanced at. | 18f | 114f | 18f | 150f | 5.0s | 38 |
| featureWalkthroughs and multi-step sequences. | 20f | 200f | 20f | 240f | 8.0s | 26 |
Of the 77 components in the library, 75 sit on one of those four. The remaining 2 declare a length of their own — Comparison Race Chart (27/306/27, 12s) and Source Highlight (18/252/18, 9.6s) — because the content sets the length rather than the other way round. They still keep the enter/hold/exit shape, which is what the overlap arithmetic actually reads. A custom length composes fine; a custom contract would not.
Four curves
An easing curve is the difference between a thing that moves and a thing that has weight. There are four here and there is deliberately no fifth: a library where every component picks its own bezier is a library that never looks like one system.
| Curve | Control points | Its one job |
|---|---|---|
| enter | cubic-bezier(0.16, 1, 0.3, 1) | Things arriving on screen. Fast out of the gate, long settle. |
| exit | cubic-bezier(0.7, 0, 0.84, 0) | Things leaving. Reluctant, then quick — the mirror of enter. |
| travel | cubic-bezier(0.45, 0, 0.55, 1) | Continuous motion that starts and ends at rest: pans, sweeps, counters. |
| accent | cubic-bezier(0.34, 1.4, 0.64, 1) | Emphasis. Slight overshoot for weight without bounce. |
Four springs
A curve is authored motion: you decide where it is at every moment. A spring is simulated motion: you decide how heavy the thing is and how hard it is pulled, and the frames fall out of that. Springs are for when something should feel physical — reach for a curve first, and a spring when the object is meant to have mass.
| Spring | Damping | Stiffness | Mass | Reach for it when |
|---|---|---|---|---|
| settle | 18 | 180 | 0.9 | The default entrance. Settles in about 18 frames with no visible bounce. |
| snap | 22 | 300 | 0.7 | Crisper. Small elements and staggered lists. |
| glide | 26 | 90 | 1.4 | Heavier, cinematic. Full-frame moves and device mockups. |
| pop | 11 | 240 | 0.8 | Deliberate overshoot. One per scene at most. |
A spring entrance is still fitted to the budget’s enter window and still gated by its exit window, so choosing one changes how a component arrives and never how long it runs or how it hands over.
The handoff
Everything above exists for this. Two components placed one after the other do not butt-join: the second one starts early, and how early is not a taste decision. It starts one of its own enter windows before the outgoing component’s hold ends, so the frames it spends arriving come out of that hold:
start(n+1) = start(n) + enter(n) + hold(n) − enter(n+1)The arithmetic lines up exactly: the incoming component reaches full presence on the very frame the outgoing one begins its exit, so that exit is never seen. It plays underneath, at full strength, while the next thing dissolves in on top of it. The two are on screen together for the incoming enter plus the outgoing exit — 36 frames where both are scene budgets, shaded in the figure below.
The overlap comes out of the hold and not out of the exit, and that was a correction rather than the first design. Overlapping into the exit looks right written down — one presence curve falls while the other rises — but it measures badly. Both scenes are mid-fade at the midpoint, so less is on screen than in either scene at rest, and the cut visibly dims. Mean luminance across real handoffs is what showed it. Overlapping into the hold keeps the outgoing scene at full strength underneath the whole time, and the dip never happens.
In an edit suite this is a dissolve and you would drag the handles by eye. Here the handle length is not a choice anyone makes at assembly time — it is the incoming clip’s enter window, which the component already declared.
Where the numbers live
The grammar itself is one file, src/registry/timing.ts, and it travels with any component you copy — the CLI brings it along with the component. The tallies above are counted off the catalog at build time. The same values are served as data for anything assembling a shot list without a person in the loop.
- /registry.json — the budgets, springs and curve names alongside every component.
- For agents — the MCP tool that returns this grammar, so a generated sequence uses the same vocabulary the components were authored against.
- All 77 components — each one shows its declared budget on its page.