Upgrading existing consumers? See the engine and state migration guide.
Select custom state
useTimelineSelector reads immutable settled state without cloning the engine’s
snapshot. Primitive selections use Object.is; arrays and plain objects compare
their own enumerable fields shallowly. An optional second argument supplies a
custom equality function. Unchanged tracks and clips retain their identities
across unrelated edits, so a selector can subscribe to one document entity.
import { useTimelineSelector } from '@techsquidtv/canvas-timeline-react';
export function TrackCount() { const count = useTimelineSelector((state) => state.tracks.length); return <output>{count} tracks</output>;}Selectors do not subscribe to per-frame playback or edit previews. Use
useTimelinePlayheadTime for live playback readouts. Keep selectors pure and
change document data through commands; hook snapshots are readonly.
Choose a domain hook
The React package exposes hooks for binding product chrome, interaction layers,
media monitors, and accessible controls to a shared TimelineEngine.
This page explains how to choose timeline hooks imported from
@techsquidtv/canvas-timeline-react/hooks; the maintained catalog lives in the
React registry Hook groups, with
category-grouped API references on each timeline hook group page. Primitive
hooks from other React subpaths, such as useRangeScrollbar() from
@techsquidtv/canvas-timeline-react/range-scrollbar, are documented with their
own primitive registry entries.
The four visible hook groups are curated task groups. The API rows inside those
groups are populated from the docs app’s hook metadata, and the registry
verifier checks that metadata against the public
@techsquidtv/canvas-timeline-react/hooks export barrel. Internal helper hooks
are intentionally excluded until they are exported as public APIs.
Use the timeline hook registry groups as the source of truth:
- Timeline State: engine context, state snapshots, active clips/layers, viewport data, ruler ticks, clip geometry, and visible clip queries.
- Editing Hooks: typed edit commands, shared edit previews, range selection, clip, clip group, track, marker, selection, clipboard, history, track header, edit consequences, existing-clip drag/drop, and app-owned media drops onto the timeline.
- Playback Controls: transport state, live playhead time, snapping, media synchronization, and clip playback effects.
- Accessible Controls: Base UI-compatible sliders, viewport scrollbar adapters, pan/zoom controls, and constant-DOM clip navigation.
Start with the narrowest hook that matches the component. Toolbar buttons that
mutate timeline structure should use command hooks such as
useTimelineEditCommands(); toolbar state can compose snapshot hooks such as
useTimelinePlayback() or useTimelineSelection(). Readouts that must follow
scrubbing or playback should use focused live hooks such as
useTimelinePlayheadTime(), useActiveMarkers(), or
useTimelineClipDropFeedback(). Use useTimeline() directly only when a
component needs both the shared engine and the synchronized state snapshot.
Edit command hooks read the current engine state when invoked, including commands issued consecutively in one event handler. History availability and viewport bounds have focused subscriptions: changing content or zoom limits updates controls even when the current scroll or zoom value does not change.
Mutating domain commands return TimelineCommandResult, so custom controls can
branch on ok and machine-readable failure reasons without reading private
engine state. Command wrappers convert TypeError and RangeError to
invalid-input results and rethrow other errors. Control callbacks such as onValueChange
remain void UI callbacks, and media startup/rate commands remain asynchronous.
Custom ruler surfaces
Use useTimelineRulerTicks() when rendering ruler ticks with DOM, SVG, or a
custom drawing system. It consumes the same core geometry as CanvasRenderer
and subscribes only to viewport changes. Choose the display with the explicit
format discriminant:
const ticks = useTimelineRulerTicks({ format: 'timecode', frameRate: { numerator: 30000, denominator: 1001 }, minimumMajorTickSpacing: 96,});Use { format: 'seconds' } for elapsed clock labels or
{ format: 'frame-number', frameRate } for absolute frame numbers. Frame-aware
rulers derive their major intervals from the project rate and return
major, medium, and minor visual weights. DOM renderers should set
minimumMajorTickSpacing from their typography and handle label alignment at
the viewport edges; the canvas renderer performs both measurements
automatically.
The React DOM timeline demo provides a live format selector and renders all three modes from this hook.
Edit command hooks
Use useTimelineEditCommands() for product chrome and custom gestures that
need to validate, preview, commit, or cancel timeline edits. The hook calls
TimelineEngine command APIs; the engine resolves snapping, ripple, overwrite,
split, range, history, and event consequences.
Compose the focused hooks around it:
- Keep toolbar tool selection in application state with React
useState. useTimelineEditPreview()subscribes to command preview validity and command state.useTimelineEditImpacts()subscribes to affected-clip consequences for live guides, renderer overlays, and drag-time affordances.useTimelineRangeSelection()adapts In/Out points todelete-rangeandlift-rangecommands. ItssetRange()replaces both boundaries atomically; Core consumers can callengine.setInOutRange(startTime, endTime). Invalid ranges return a command failure without changing either boundary.useTimelineClipGroups()reads timeline clip groups and exposes group, ungroup, and selected-group commands for editor chrome.
import { fromSeconds } from '@techsquidtv/canvas-timeline-utils';import { useTimelineSelection, useTimelineEditCommands, useTimelineEditImpacts, useTimelineEditPreview, useTimelineRangeSelection,} from '@techsquidtv/canvas-timeline-react/hooks';
function EditToolbar() { const { selectedClip } = useTimelineSelection(); const editCommands = useTimelineEditCommands(); const editPreview = useTimelineEditPreview(); const editImpacts = useTimelineEditImpacts(); const rangeSelection = useTimelineRangeSelection();
return ( <> <button disabled={!selectedClip} onClick={() => selectedClip && editCommands.moveClip({ clipId: selectedClip.id, startTime: fromSeconds(4), }) } > Move </button> <button disabled={!rangeSelection.hasRange} onClick={() => rangeSelection.liftRange()}> Lift </button> {editPreview.previewing && <span>{editImpacts.impacts.length} affected</span>} </> );}External media playback
Start with the adapter-owned React hooks for normal preview surfaces:
import { useHTMLTimelineMedia } from '@techsquidtv/canvas-timeline-html-media-adapter/react';import { useMediabunnyTimelineMedia } from '@techsquidtv/canvas-timeline-mediabunny-adapter/react';These hooks create and dispose the imperative adapter, reconcile source
definitions, and compose useTimelineMediaSync() for media-aware play, pause,
rate, In/Out, and loop behavior. They accept ordinary inline URL source arrays;
stable array identity is not required for unchanged descriptors. If a hook must
replace its imperative adapter, synchronized transport pauses, any pending start
is cancelled, and the replacement is primed at the current paused position.
Use useTimelineMediaSync() when your app implements a different
TimelineMediaSyncAdapter. It owns active-layer lookup, initial-content seeking,
paused preview seeks, structured TimelineMediaError reporting, and shared Core
range policy. The adapter remains responsible for decoding, rendering, audio
scheduling, and its external clock. Pass the underlying controller as
adapterIdentity when changing that controller should cancel pending transport
and release its clock. Omit it for an inline callback facade; the facade can be
recreated without useMemo boilerplate.
Drop down to useTimelineMediaPlayback() only for direct external-clock
coordination. At that level, playbackOptions contains range policy but not a
loop boolean. Supply loop(timelineTime, activeLayers) to enable looping and
realign the clock; requiring the callback prevents an external clock from
looping without a seek/restart implementation. Supply onError(error) to
observe layer synchronization and loop-realignment failures; the hook serializes asynchronous
adapter work, waits for startup and rate synchronization before resolving those
commands, and stops both clocks before reporting a failure. Loop realignment is
also awaited before another external-clock tick. The high-level hook orders
paused preview seeks, startup, loop realignment, layer synchronization, and rate
changes through one adapter-operation queue, so a slow decoder operation cannot
apply an older timeline position after newer transport work. Concurrent
high-level play requests share one startup; pausing during startup resolves the
pending play result with reason: 'cancelled' and prevents stale adapter work
from starting the timeline. The higher-level sync hook wraps failures as
TimelineMediaError values while preserving specific reasons such as
loop-failed.
Read Media adapters for source ownership, proxy versus fallback semantics, readiness, lazy loading, timing, track selection, audio, and error handling.
Accessibility and keyboard composition
Canvas Timeline keeps repeated clips and track visuals on canvas for performance, while low-count interaction chrome stays in DOM. The hook layer is the bridge between those two surfaces.
Use control adapters when composing semantic UI:
useTimelinePlayheadControl(),useTimelineInOutRangeControl(),useTimelineZoomControl(), anduseTimelinePanControl()return Base UI-compatible props and formatted value text.useTimelineViewportRangeControl()adapts viewport state to generic range scrollbar primitives.useTimelineVerticalRangeControl()adapts track-stack scroll state to the same primitives.useTimelineClipNavigation()exposes one active canvas clip at a time instead of requiring one DOM node per clip.useTimelineKeyboard()returns focus-scoped keyboard shortcut props for opt-in editor transport, mark, marker, snapping, and zoom commands.useTimelineTrack()anduseTimelineTrackHeader()expose one track’s first-party state, canvas-aligned row geometry, and DOM-ready header props for custom track header compositions.useTimelineKeyframes(),useTimelineKeyframeGeometry(),useTimelineKeyframeDrag(),useTimelineKeyframeSegments(), anduseTimelineKeyframeTangentDrag()expose headless keyframe geometry, tangent handles, commands, property evaluation, and drag previews for custom keyframe editors.
Control adapter onValueCommitted callbacks receive the committed value plus
opaque TimelineControlCommitDetails forwarded from the underlying control
primitive. Canvas Timeline settles the engine before forwarding that metadata,
but does not inspect it; narrow the details in application code when a specific
control library documents extra fields.
Keyboard behavior from these hooks is opt-in and scoped to the element that spreads the returned props. Canvas Timeline does not install global hotkeys or override application shortcuts.
For shadcn-like composition, wrap the interactive editor region instead of registering document-level listeners:
<Timeline.KeyboardScope frameRate={24}> <Timeline.Root> <Timeline.ClipInteractionLayer /> <Timeline.PlayheadArea /> <Timeline.PlayheadGrabber /> </Timeline.Root></Timeline.KeyboardScope>Pass bindings when your product owns a different shortcut map. Custom bindings
replace the preset so applications can avoid collisions with their own command
system. Frame-step shortcuts are included only when frameRate is supplied. Use
platform when tests, server rendering, or product settings need deterministic
platform-specific preset bindings.
Related guides
- React registry Hook groups for the grouped timeline hook pages, examples, and API links.
- Tracks and clips for clip movement, cross-track drag, and drop feedback hooks.
- Keyframes for clip property keyframes, interpolation modes, and keyframe interaction composition.
- Media adapters for native and decoded preview hooks, source registries, and external-clock transport.
- Canvas renderer customization for custom drawing layers, renderer theme options, and canvas overlays.
Track organization and bulk deletion
Use useTimelineTrackCommands() for menus that only issue commands. It does not subscribe to document or viewport changes. useTimelineTracks() adds collection state; useTimelineTrack(id) adds one row’s state and geometry.
const tracks = useTimelineTrackCommands();tracks.renameTrack('audio', 'Dialogue');tracks.moveTrack('audio', 0); // Final zero-based row index.tracks.setCollapsed('audio', true); // Preserves expanded height and media visibility.tracks.toggleCollapse('audio');
const edits = useTimelineEditCommands();edits.deleteClips(['clip-a', 'clip-b']);edits.deleteSelectedClips(); // Reads selection when invoked.Rename, reorder, and collapse each create one undo step. Unchanged values create none. Bulk deletion includes linked clip groups and validates all affected tracks before mutation; a locked track rejects the entire operation. Subscribe to useTimelineSelection() separately for button availability.
Headless clip trimming
useTimelineClipTrim() provides startClipTrim, moveClipTrim, endClipTrim, and cancelClipTrim. It captures the clip edge and zoom once, previews through Core without mutating the document, and revalidates the final edit before committing one history step. It cancels its preview on unmount and preserves previews replaced by another interaction. The packaged Timeline.ClipInteractionLayer uses this same hook.
const trim = useTimelineClipTrim();
<button aria-label="Trim clip end" onPointerDown={(event) => { const started = trim.startClipTrim({ clipId, edge: 'end', clientX: event.clientX }); if (started.ok) event.currentTarget.setPointerCapture(event.pointerId); }} onPointerMove={(event) => { trim.moveClipTrim({ clientX: event.clientX }); }} onPointerUp={() => { trim.endClipTrim(); }} onPointerCancel={() => { trim.cancelClipTrim(); }} onLostPointerCapture={() => { trim.cancelClipTrim(); }}/>;Keep touch drag handles at touch-action: none. Use useTimelineEditPreview() for live preview details. trimming only changes at lifecycle boundaries; pointer moves do not cause this hook to rerender. For programmatic edits without a gesture, use useTimelineEditCommands().trimClip().
Keyboard command composition
useTimelineKeyboard() and Timeline.KeyboardScope accept commandHandlers to replace individual command implementations. Unspecified commands retain the preset behavior. Commands read current engine state without subscribing to timeline updates.
<Timeline.KeyboardScope frameRate={24} commandHandlers={{ togglePlayback: () => (media.playing ? media.pause() : media.play()), }} onCommandResult={(command, result) => { if (result && !result.ok) showStatus(command, result.message); }} onCommandError={(error) => showError(error.message)}> {/* Focusable timeline surface */}</Timeline.KeyboardScope>Use the transport returned by useTimelineMediaSync() or an adapter hook for media-aware playback. Async handlers preserve their pending operation and structured media result; repeated Space keydown events are ignored. Handlers may also return void when the app owns status reporting. Unexpected errors go to onCommandError, or browser error reporting when omitted.
The hook’s executeCommand(command) uses the same implementations and returns their result or promise for custom menus. It does not apply keyboard focus rules or invoke the keyboard result/error callbacks; the caller handles that result. Native inputs, buttons, and other interactive controls retain their own shortcuts.