Timeline

10 parts

Root 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

Loading demo…

Anatomy

Import the parts and compose them. Each part forwards attributes to its underlying element.

vue
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

Root

Root 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.

PropTypeDescription
defaultTracks?TimelineTrack[]Uncontrolled initial tracks (ignored when v-model:tracks is bound).
defaultClips?TimelineClip[]Uncontrolled initial clips.
defaultMarkers?TimelineMarker[]Uncontrolled initial markers.
defaultCurrentTime?numberUncontrolled initial playhead time (seconds).
defaultOffset?numberUncontrolled initial left-edge time (seconds).
defaultPxPerSecond?numberUncontrolled initial zoom in pixels-per-second.
defaultSelectedClipIds?string[]Uncontrolled initial selected clip ids.
duration?numberTotal content duration in seconds. When omitted it is auto-derived from the clips (largest start + duration).
minPxPerSecond?numberMinimum zoom in pixels-per-second; pxPerSecond is clamped to this.
maxPxPerSecond?numberMaximum zoom in pixels-per-second.
fps?numberFrame rate (timecode + frame snapping + keyboard nudge).
snapStep?numberSnap step in seconds. When omitted it defaults to one frame (1 / fps).
snapping?booleanEnable magnetic snapping to clip edges / playhead / markers / grid.
snapThresholdPx?numberSnap radius in pixels.
trackHeight?numberDefault track-lane height in pixels (fixed; NOT zoomed).
disabled?booleanDisable all interaction.
dir?DirectionWriting 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?numberTwo-way bound value (v-model:currentTime).
offset?numberTwo-way bound value (v-model:offset).
pxPerSecond?numberTwo-way bound value (v-model:pxPerSecond).
selectedClipIds?string[]Two-way bound value (v-model:selectedClipIds).
Emits
EventPayload
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

Ruler

The 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.

PropTypeDescription
mode?'seconds' | 'timecode' | 'frames'Tick label rendering mode.
targetDensity?numberTarget pixel spacing between ticks.

TimelineTracks

Tracks

The 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.

PropTypeDescription
marquee?booleanWhether marquee selection requires holding Shift. - true (default): plain drag on empty space draws a marquee. - false: only Shift+drag draws a marquee.
requireShift?booleanRequire Shift to start a marquee.

TimelineTrack

Track

One 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.

PropTypeDescription
trackIdstringId of the track this lane renders.

TimelineTrackHeader

TrackHeader

The 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`.

PropTypeDescription
minHeight?numberMinimum lane height (px) the resize drag clamps to.
maxHeight?numberMaximum lane height (px) the resize drag clamps to.

TimelineClip

Clip

A 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.

PropTypeDescription
clipIdstringId of the clip this block renders.

TimelineClipHandle

ClipHandle

A 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.

PropTypeDescription
side'start' | 'end'Which edge this handle trims.

TimelinePlayhead

Playhead

The 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.

PropTypeDescription
label?stringAccessible label.

TimelineMarker

Marker

A marker pin at `marker.time`. Positioned at `scale(marker.time)`. It is a roving-focus stop, `role="button"` with an `aria-label` (label + timecode).

PropTypeDescription
markerIdstringId of the marker this pin renders.

TimelineSelection

Selection

No props or events — renders its element and forwards attributes.