MOTIONSTASH

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.

The four timing budgets at 30fps, with the number of components using each
BudgetEnterHoldExitTotalSecondsIn use
beatLower thirds, badges, captions. Something you glance at.12f36f12f60f2.0s2
statementTitle cards, kinetic headlines. One line you actually read.15f60f15f90f3.0s9
sceneProduct shots, charts — anything read rather than glanced at.18f114f18f150f5.0s38
featureWalkthroughs and multi-step sequences.20f200f20f240f8.0s26

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.

The four easing curves and their control points
CurveControl pointsIts one job
entercubic-bezier(0.16, 1, 0.3, 1)Things arriving on screen. Fast out of the gate, long settle.
exitcubic-bezier(0.7, 0, 0.84, 0)Things leaving. Reluctant, then quick — the mirror of enter.
travelcubic-bezier(0.45, 0, 0.55, 1)Continuous motion that starts and ends at rest: pans, sweeps, counters.
accentcubic-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 presets and their configurations
SpringDampingStiffnessMassReach for it when
settle181800.9The default entrance. Settles in about 18 frames with no visible bounce.
snap223000.7Crisper. Small elements and staggered lists.
glide26901.4Heavier, cinematic. Full-frame moves and device mockups.
pop112400.8Deliberate 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.

Scene A — scene budget, 150fhold 114fenter 18fexit 18fScene B — starts at frame 114, 150fhold 114fenter 18fexit 18fframe 132 — B fully present, A begins its exit0frame 264 · 8.8s
Two scene budgets in sequence. Butt-joined they would run 300 frames; overlapped they run 264, and the 36 frames in the shaded band are the only ones where both are on screen.

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.