Timeline
10 partsRoot of the headless multi-track timeline (video/audio editor). It owns the
track / clip / marker data plus the playhead (currentTime), horizontal scroll
(offset), zoom (pxPerSecond), and selection — all two-way via v-model or
uncontrolled via default*. The public arrays are reconciled into internal
shallowRef Maps for O(1) reads and identity-stable per-item computeds.
The COORDINATE MODEL is load-bearing: pxPerSecond is applied as real
horizontal LAYOUT pixels (clip widths are genuine px, never a CSS
scale(zoom)), so a useScale projects the visible window
[offset, offset + width/pxPerSecond] → [0, width]. The vertical axis is
fixed-height track lanes and is NOT zoomed.
Transient drag/trim/scrub positions are written to an in-flight overlay and
committed to the model on pointerup (commitMutation); an external v-model
write during a gesture is ignored (the isMutating early-return) so it never
clobbers the live drag. Granular @clips-change / @tracks-change are emitted
alongside v-model so consumers may own their data via applyClipChanges /
applyTrackChanges.
Provides TimelineContext to every part: the scale, the shared snap engine
(clip edges + playhead + markers + grid), the clip/track/playhead actions, and
the roving-focus registry.
Demo
Anatomy
Import the parts and compose them. Each part forwards attributes to its underlying element.
import {
TimelineRoot,
TimelineRuler,
TimelineTracks,
TimelineTrack,
TimelineTrackHeader,
TimelineClip,
TimelineClipHandle,
TimelinePlayhead,
TimelineMarker,
TimelineSelection,
} from '@robonen/primitives/canvas/timeline';
<TimelineRoot>
<TimelineRuler />
<TimelineTracks />
<TimelineTrack />
<TimelineTrackHeader />
<TimelineClip />
<TimelineClipHandle />
<TimelinePlayhead />
<TimelineMarker />
<TimelineSelection />
</TimelineRoot>API Reference
TimelineRoot
RootRoot of the headless multi-track timeline (video/audio editor). It owns the track / clip / marker data plus the playhead (`currentTime`), horizontal scroll (`offset`), zoom (`pxPerSecond`), and selection — all two-way via `v-model` or uncontrolled via `default*`. The public arrays are reconciled into internal `shallowRef` Maps for O(1) reads and identity-stable per-item computeds. The COORDINATE MODEL is load-bearing: `pxPerSecond` is applied as real horizontal LAYOUT pixels (clip widths are genuine px, never a CSS `scale(zoom)`), so a `useScale` projects the visible window `[offset, offset + width/pxPerSecond]` → `[0, width]`. The vertical axis is fixed-height track lanes and is NOT zoomed. Transient drag/trim/scrub positions are written to an in-flight overlay and committed to the model on pointerup (`commitMutation`); an external `v-model` write during a gesture is ignored (the `isMutating` early-return) so it never clobbers the live drag. Granular `@clips-change` / `@tracks-change` are emitted alongside `v-model` so consumers may own their data via `applyClipChanges` / `applyTrackChanges`. Provides `TimelineContext` to every part: the scale, the shared snap engine (clip edges + playhead + markers + grid), the clip/track/playhead actions, and the roving-focus registry.
| Prop | Type | Description |
|---|---|---|
defaultTracks? | TimelineTrack[] | Uncontrolled initial tracks (ignored when v-model:tracks is bound). |
defaultClips? | TimelineClip[] | Uncontrolled initial clips. |
defaultMarkers? | TimelineMarker[] | Uncontrolled initial markers. |
defaultCurrentTime? | number | Uncontrolled initial playhead time (seconds). |
defaultOffset? | number | Uncontrolled initial left-edge time (seconds). |
defaultPxPerSecond? | number | Uncontrolled initial zoom in pixels-per-second. |
defaultSelectedClipIds? | string[] | Uncontrolled initial selected clip ids. |
duration? | number | Total content duration in seconds. When omitted it is auto-derived from the
clips (largest start + duration). |
minPxPerSecond? | number | Minimum zoom in pixels-per-second; pxPerSecond is clamped to this. |
maxPxPerSecond? | number | Maximum zoom in pixels-per-second. |
fps? | number | Frame rate (timecode + frame snapping + keyboard nudge). |
snapStep? | number | Snap step in seconds. When omitted it defaults to one frame (1 / fps). |
snapping? | boolean | Enable magnetic snapping to clip edges / playhead / markers / grid. |
snapThresholdPx? | number | Snap radius in pixels. |
trackHeight? | number | Default track-lane height in pixels (fixed; NOT zoomed). |
disabled? | boolean | Disable all interaction. |
dir? | Direction | Writing direction. When omitted it is inherited from the nearest
ConfigProvider (falling back to 'ltr'); an explicit value wins. |
tracks? | TimelineTrack[] | Two-way bound value (v-model:tracks). |
clips? | TimelineClip[] | Two-way bound value (v-model:clips). |
markers? | TimelineMarker[] | Two-way bound value (v-model:markers). |
currentTime? | number | Two-way bound value (v-model:currentTime). |
offset? | number | Two-way bound value (v-model:offset). |
pxPerSecond? | number | Two-way bound value (v-model:pxPerSecond). |
selectedClipIds? | string[] | Two-way bound value (v-model:selectedClipIds). |
| Event | Payload |
|---|---|
update:tracks | [value: TimelineTrack[]] |
update:clips | [value: TimelineClip[]] |
update:markers | [value: TimelineMarker[]] |
update:currentTime | [value: number] |
update:offset | [value: number] |
update:pxPerSecond | [value: number] |
update:selectedClipIds | [value: string[]] |
TimelineRuler
RulerThe time axis sitting above the tracks. It embeds a {@link TimeRulerRoot} bound to the SAME `offset` / `pxPerSecond` / `fps` as the timeline (so the ruler and the lanes share one scale), and turns clicks / drags on the axis into playhead scrubs. The default slot forwards `{ ticks, majorTicks, minorTicks, scale, formatTime }` from the embedded ruler so consumers render their own tick layer.
| Prop | Type | Description |
|---|---|---|
mode? | 'seconds' | 'timecode' | 'frames' | Tick label rendering mode. |
targetDensity? | number | Target pixel spacing between ticks. |
TimelineTracks
TracksThe scrollable lanes container: a vertical stack of `TimelineTrack`s and the surface that hosts the marquee selection layer. It registers itself as the timeline's `viewportEl` (the scale's range origin) and measures its width so the shared `useScale` projects correctly. Shift + drag on empty space (or plain drag, configurable) draws a selection rectangle and selects every clip whose time span intersects it on release — ported from the flow `useMarquee` pattern but operating in the timeline's 1-D-time × track-lane space.
| Prop | Type | Description |
|---|---|---|
marquee? | boolean | Whether marquee selection requires holding Shift.
- true (default): plain drag on empty space draws a marquee.
- false: only Shift+drag draws a marquee. |
requireShift? | boolean | Require Shift to start a marquee. |
TimelineTrack
TrackOne track lane. Looks its track record up from the Root by `trackId`, applies the resolved (fixed, non-zoomed) lane height, and provides `TimelineTrackContext` to its header + clips. `role="row"` within the `TimelineTracks` grid; surfaces `data-muted` / `data-locked` / `data-soloed`. The default slot receives the track record + flags.
| Prop | Type | Description |
|---|---|---|
trackId | string | Id of the track this lane renders. |
TimelineTrackHeader
TrackHeaderThe track gutter: the label plus mute / lock / solo toggle buttons and a drag-resize affordance for the lane height. The default slot exposes the track record and ready-made `toggle*` handlers + flag booleans so a consumer can render its own buttons; the `#resize-handle` slot wires the height drag. `role="rowheader"`; `data-muted` / `data-locked` / `data-soloed` mirror the lane. The toggle buttons are native `<button>`s with `aria-pressed`.
| Prop | Type | Description |
|---|---|---|
minHeight? | number | Minimum lane height (px) the resize drag clamps to. |
maxHeight? | number | Maximum lane height (px) the resize drag clamps to. |
TimelineClip
ClipA clip block. Positioned by the shared scale (`left = scale(start)`, `width = scale(start + duration) - scale(start)` — real layout pixels, never a CSS transform-scale). It is a roving-focus stop (single Root tab-stop → roving between clips in time order), `role="group"` / `aria-roledescription="clip"` with a descriptive label (label + start/duration timecode), `aria-selected`, and `data-selected` / `data-dragging` / `data-locked`. Dragging (via `usePointerDrag`) converts the pointer x-delta to a time delta (snapped to clip edges / playhead / markers / grid) for a move, and the y-delta crosses lanes (drop onto another track). Keyboard: Arrow Left/Right nudge the selected clip(s) by a frame (Shift = 1 s), Arrow Up/Down move across tracks, Delete removes.
| Prop | Type | Description |
|---|---|---|
clipId | string | Id of the clip this block renders. |
TimelineClipHandle
ClipHandleA trim handle on a clip edge. It is `role="slider"` (horizontal) over the clip's trim range in seconds, with `aria-valuemin` / `aria-valuemax` / `aria-valuenow` and an `aria-valuetext` timecode — so trims are keyboard operable AND announced. Dragging trims the start edge (`side: 'start'`) or the end edge (`side: 'end'`), snapped to clip edges / playhead / markers / grid and clamped so duration stays `> 0`. Arrow keys trim by one frame (Shift = 1 s). Render it inside a `TimelineClip`'s default slot, absolutely positioned at the clip's left / right edge.
| Prop | Type | Description |
|---|---|---|
side | 'start' | 'end' | Which edge this handle trims. |
TimelinePlayhead
PlayheadThe canonical playhead scrubber. `role="slider"` over `[0, duration]` with `aria-valuenow` = `currentTime`, an `aria-valuetext` timecode, horizontal orientation, and the default `aria-label` `'Playhead'`. Positioned at `scale(currentTime)`. Drag scrubs (snapped to the frame grid); Arrow keys step one frame (Shift = 1 s), Home / End jump to 0 / duration. It is the Root's single keyboard scrub stop — `tabindex` 0 when interactive.
| Prop | Type | Description |
|---|---|---|
label? | string | Accessible label. |
TimelineMarker
MarkerA marker pin at `marker.time`. Positioned at `scale(marker.time)`. It is a roving-focus stop, `role="button"` with an `aria-label` (label + timecode).
| Prop | Type | Description |
|---|---|---|
markerId | string | Id of the marker this pin renders. |
TimelineSelection
SelectionNo props or events — renders its element and forwards attributes.