Upgrading existing consumers? See the engine and state migration guide.
Canvas Timeline is built as a layered, modular framework for timeline-based video, audio, and animation editors. Before diving into the packages or code, it helps to understand how the core engine, the rendering pipeline, the React bindings, and your application’s metadata fit together.
System Architecture
The following map illustrates how state, mutations, rendering, and application metadata interact across the Canvas Timeline ecosystem:
Your Application
UI & Rendering Layers
Core Engine
Subsystem Guides
To understand specific areas of the model and integration layers, refer to these detailed guides:
- Rational time — Learn about fraction-based time representation and arithmetic helpers.
- Tracks and clips — Understand tracks and clips configuration, move commands, and dragging hooks.
- Keyframes — Animate clip properties with engine-level keyframes and headless interaction hooks.
- Events and lifecycle — Retrieve active playhead layers, subscribe to events, and use metadata.
- React editor hooks — Bind timeline state using domain hooks and accessibility wrappers.
- Media adapters — Resolve source inputs, proxies, fallbacks, lazy loading, and external-clock transport.
Timeline state
Canvas Timeline centers on a serializable TimelineState object. This state acts as the single source of truth and the contract between the engine, React bindings, canvas renderer, and any persistence layer in your application.
At the top level, TimelineState contains:
| Field | Type | What it represents |
|---|---|---|
tracks |
Track[] |
Ordered lanes and their clips. |
clipGroups |
TimelineClipGroup[] |
Clip ids edited together as linked timeline groups. |
playheadTime |
RationalTime |
The current edit or playback cursor. |
zoomScale |
number |
Horizontal scale in pixels per second. |
scrollLeft |
number |
Horizontal viewport offset in pixels. |
markers |
TimelineMarker[] |
Optional annotations pinned to timeline time. |
inPoint / outPoint |
RationalTime | undefined |
Optional range boundaries for loop regions and edit selection. |
duration |
RationalTime | undefined |
Optional fixed sequence duration. |
The engine owns the live structural state and emits events when edits, playback, or viewport changes occur.
import { TimelineEngine, fromSeconds } from '@techsquidtv/canvas-timeline';
const engine = new TimelineEngine({ duration: fromSeconds(30), tracks: [ { id: 'track-visual-1', kind: 'visual', name: 'Visual 1', selected: false, locked: false, muted: false, visible: true, clips: [], targeted: true, }, ],});Decoupling Application Metadata
This separation provides significant performance benefits:
- Lightweight History: Canvas Timeline manages a full undo/redo stack. Keeping the state model small allows snapshots to be cloned and serialized in milliseconds.
- Normalized Media Data: If the same source asset is used 10 times in a timeline, all 10 clips share a single
sourceId. Your app needs to load and cache the asset’s duration, dimensions, waveform, thumbnails, or transcript only once.
Use the table below to guide where metadata should reside:
| Key Type | What to Store | Examples |
|---|---|---|
sourceId |
Shared assets and media file attributes | Video URLs, durations, dimensions, waveform caches, transcripts. |
clip.id |
Clip-specific editorial overrides and presets | Volume levels, visual crops, color grading presets, text captions, comments. |
track.id |
App-specific lane metadata | Audio routing, record arm, owners, language, meters, automation mode. |
// Example: Resolving assets from a separate storeinterface AssetMeta { url: string; thumbnailUrl?: string;}
const assetMetadata = new Map<string, AssetMeta>([ ['asset-interview-1', { url: '/media/interview.mp4', thumbnailUrl: '/thumbs/interview.jpg' }],]);
// Inside your rendering loop or React component:const asset = assetMetadata.get(clip.sourceId);Track-level state belongs in Track only when Canvas Timeline itself needs to
understand it for rendering, media lookup, editing, history, or shared hooks. Use
an app-owned store for product-specific header controls:
interface AudioTrackMeta { recordArmed: boolean; routeId: string; meterSourceId: string;}
const audioTrackMetaById: Record<string, AudioTrackMeta> = { 'audio-1': { recordArmed: false, routeId: 'mix-bus', meterSourceId: 'meter-audio-1', },};Rendering layers
Canvas Timeline splits rendering responsibilities to optimize performance:
- Canvas Layer: Renders dense timeline graphics (ruler, time ticks, tracks, clips, In/Out range fill, snap points, etc.). Rendering hundreds of clips in DOM nodes triggers browser reflows and input lag; drawing to a 2D Canvas keeps interactions at 60fps.
- React Layer: Coordinates layout structure, pointer events, click detection, scrollbar elements, and delegated editing grabbers. React excels at context injection, focus, and state bindings.
CSS styles interaction layers and chrome; renderer theme styles canvas-painted timeline visuals. This keeps shadcn-style token control available without moving clips, tracks, rulers, or markers out of the canvas rendering path.
| Surface | Controlled by | Examples |
|---|---|---|
| Canvas visuals | Renderer theme | Clip fills, clip labels, track lanes, ruler ticks, markers, snap lines. |
| Interaction layers | React and CSS | Scrollbars, the single active clip affordance, playhead grabbers, and In/Out grabbers. |
| Product chrome | Your app CSS | Toolbars, inspectors, panels, menus, dialogs, and keyboard-focus styling. |
| Project color data | Timeline state | Per-clip color metadata that overrides renderer clip background colors. |
| Layer | Responsibility | Output/API |
|---|---|---|
| Core engine | State transitions, editing commands, playback loops, history, snapping math. | TimelineEngine |
| Renderer | Double-buffered Canvas drawing of ticks, backgrounds, waveforms, ruler. | CanvasRenderer |
| React package | Layout components, interaction surfaces, scroll/drag hooks, Provider context. | Timeline.Root, useTimeline |
| Product application | Media decoding, project persistence, hotkeys, asset panel, styling themes. | Your application |
Package boundaries
The easiest way to start is importing @techsquidtv/canvas-timeline. However, the code is separated into individual sub-packages so you can import only what your app needs:
| Package | Use it when |
|---|---|
@techsquidtv/canvas-timeline-core |
You need the data model, editing math, and playback engine in a non-React or node environment. |
@techsquidtv/canvas-timeline-react |
You are binding your custom UI or elements to the engine hooks and context. |
@techsquidtv/canvas-timeline-html-media-adapter |
You want one native HTMLMediaElement to follow timeline media. |
@techsquidtv/canvas-timeline-mediabunny-adapter |
You want Mediabunny to decode, render, or schedule media from timeline clips. |
@techsquidtv/canvas-timeline-renderer |
You want to configure, subclass, or theme the canvas drawing pipeline. |
@techsquidtv/canvas-timeline-utils |
You only need rational-time arithmetic or helper functions. |
Media packages follow the same boundary rule as the rest of the architecture:
timeline state stays serializable, while heavy media objects stay in the app or
adapter. Framework-neutral synchronization, source timing, lifecycle, operation,
and error contracts live in Core. Clips connect to media by stable
clip.sourceId values; the application resolves an original, proxy, cache, or
review choice and gives the adapter one matching source plus optional
content-equivalent load fallbacks.
The HTML adapter maps that registry to one native media element; Mediabunny
loads registered sources on demand for decoded frame/audio preview. Both package
roots are imperative and framework-free. Their React lifecycle and transport
hooks are published from explicit ./react entry points. Start with
useHTMLTimelineMedia or useMediabunnyTimelineMedia in React, and read the
media adapter guide for proxy ownership, readiness,
preloading, timing, audio, and external-clock range behavior.
Mental model summary
When debugging or designing features, remember:
- Serializable State: State is pure data (
TimelineState). - Single Mutator: Only
TimelineEngineupdates the state. - Canvas for Density: Visuals are drawn on a canvas, avoiding DOM overhead.
- React for Interaction: Handlers, hover states, scroll events, and dialogs belong in React.
- Decoupled Data: Heavy media assets and domain details live in your app state, mapped to the timeline by stable IDs.