Timelinx

Operations Reference

Every OperationPrimitive in @timelinx/core - what it does, what it requires, and why it rejects

OperationPrimitive is a discriminated union with 42 variants. Every mutation to TimelineState is expressed as one or more operations inside a Transaction. Operations are the only way to change state.

import { dispatch } from '@timelinx/core';

const result = dispatch(state, {
  id: 'tx-1',
  label: 'Human-readable label',
  timestamp: Date.now(),
  operations: [
    { type: 'ADD_TRACK', track },
    { type: 'INSERT_CLIP', trackId: track.id, clip },
  ],
});

Operations are validated in order. Each one is checked against the state produced by all previous operations in the same transaction. If any operation rejects, the entire transaction is rejected and the original state is unchanged.


Clip Operations

MOVE_CLIP

Move a clip to a new start frame on the same track or a different track.

{ type: 'MOVE_CLIP'; clipId: ClipId; newTimelineStart: TimelineFrame; targetTrackId?: TrackId }
FieldRequiredNotes
clipIdyesMust exist.
newTimelineStartyesNew position. timelineEnd is calculated from the clip's existing duration.
targetTrackIdnoIf omitted, stays on the same track. If provided, must match clip.assetId mediaType.

Rejects with: CLIP_NOT_FOUND, TRACK_NOT_FOUND, OVERLAP, LOCKED_TRACK, TYPE_MISMATCH, OUT_OF_BOUNDS

{ type: 'MOVE_CLIP', clipId: toClipId('clip-1'), newTimelineStart: toFrame(450) }

RESIZE_CLIP

Trim one edge of a clip. The opposite edge stays fixed.

{ type: 'RESIZE_CLIP'; clipId: ClipId; edge: 'start' | 'end'; newFrame: TimelineFrame }
FieldNotes
edge'start' shrinks/grows from the left; 'end' from the right.
newFrameThe new position of the chosen edge.

When trimming, the dispatcher adjusts mediaIn (for edge: 'start') or mediaOut (for edge: 'end') to maintain the media-duration invariant at the current speed. Trimming past the asset's intrinsicDuration rejects.

Rejects with: CLIP_NOT_FOUND, LOCKED_TRACK, MEDIA_BOUNDS_INVALID, OVERLAP, INVALID_RANGE

// Extend the end of a clip by 30 frames
{ type: 'RESIZE_CLIP', clipId: toClipId('clip-1'), edge: 'end', newFrame: toFrame(330) }

SLICE_CLIP

Split a clip into two at a specific frame. The original clip is removed and replaced with two new clips.

{ type: 'SLICE_CLIP'; clipId: ClipId; atFrame: TimelineFrame }

atFrame must be strictly inside the clip (timelineStart < atFrame < timelineEnd). The dispatcher creates two clips whose IDs are clipId + '-l' and clipId + '-r'.

Prefer DELETE + INSERT for compound edits

SLICE_CLIP is a convenience operation for single-frame splits. For a razor tool that must preview ghost clips or handle undo as one action across multiple tracks, use DELETE_CLIP + INSERT_CLIP pairs in a transaction.

Rejects with: CLIP_NOT_FOUND, LOCKED_TRACK, INVALID_RANGE (if atFrame is not inside the clip)


DELETE_CLIP

Remove a clip from its track.

{ type: 'DELETE_CLIP'; clipId: ClipId }

Rejects with: CLIP_NOT_FOUND, LOCKED_TRACK


INSERT_CLIP

Place a new clip on a track.

{ type: 'INSERT_CLIP'; clip: Clip; trackId: TrackId }

The clip's assetId must exist in assetRegistry at the time of validation. The clip's trackId field is ignored - the trackId in the operation is authoritative.

Rejects with: ASSET_MISSING, TYPE_MISMATCH, OVERLAP, LOCKED_TRACK, OUT_OF_BOUNDS, DUPLICATE_ID, MEDIA_BOUNDS_INVALID

import { createClip, toFrame, toClipId, toAssetId, toTrackId } from '@timelinx/core';

{
  type: 'INSERT_CLIP',
  trackId: toTrackId('v1'),
  clip: createClip({
    id: 'clip-1',
    assetId: toAssetId('asset-interview'),
    trackId: toTrackId('v1'),
    timelineStart: toFrame(0),
    timelineEnd: toFrame(300),
    mediaIn: toFrame(0),
    mediaOut: toFrame(300),
  }),
}

SET_MEDIA_BOUNDS

Remap which portion of the asset plays without changing the clip's timeline position.

{ type: 'SET_MEDIA_BOUNDS'; clipId: ClipId; mediaIn: TimelineFrame; mediaOut: TimelineFrame }

Use this for slip edits: the clip stays in its timeline position but plays a different window of the source. The new mediaOut - mediaIn must equal the existing timelineEnd - timelineStart (adjusted for speed).

Rejects with: CLIP_NOT_FOUND, LOCKED_TRACK, MEDIA_BOUNDS_INVALID


SET_CLIP_ENABLED

{ type: 'SET_CLIP_ENABLED'; clipId: ClipId; enabled: boolean }

Disabled clips remain in the timeline but are excluded from compositing. Does not affect other clips.


SET_CLIP_REVERSED

{ type: 'SET_CLIP_REVERSED'; clipId: ClipId; reversed: boolean }

When true, the media plays from mediaOut backward to mediaIn.


SET_CLIP_SPEED

{ type: 'SET_CLIP_SPEED'; clipId: ClipId; speed: number }

speed must be > 0. Common values: 0.5 (half speed), 1.0 (normal), 2.0 (double speed). When speed changes, the dispatcher recalculates mediaOut so the timeline duration is preserved.

Rejects with: CLIP_NOT_FOUND, LOCKED_TRACK, SPEED_INVALID (if speed ≤ 0), MEDIA_BOUNDS_INVALID


SET_CLIP_COLOR

{ type: 'SET_CLIP_COLOR'; clipId: ClipId; color: string | null }

Hex color string (e.g. '#e74c3c') for visual organization in the timeline row. null clears the override.


SET_CLIP_NAME

{ type: 'SET_CLIP_NAME'; clipId: ClipId; name: string | null }

Display name override for the clip label. null falls back to the asset name.


Track Operations

ADD_TRACK

{ type: 'ADD_TRACK'; track: Track }

Appends the track to timeline.tracks. The track's ID must be unique across all tracks.

Rejects with: DUPLICATE_ID, LOCKED_TRACK (if target would be a locked parent group)

import { createTrack } from '@timelinx/core';

{ type: 'ADD_TRACK', track: createTrack({ id: 'v2', name: 'V2', type: 'video' }) }

DELETE_TRACK

{ type: 'DELETE_TRACK'; trackId: TrackId }

Removes the track and all its clips. Rejects if the track is locked.

Rejects with: TRACK_NOT_FOUND, LOCKED_TRACK


REORDER_TRACK

{ type: 'REORDER_TRACK'; trackId: TrackId; newIndex: number }

Moves the track to newIndex in the tracks array. 0 is the top of the timeline. Out-of-range indexes are clamped.


SET_TRACK_HEIGHT

{ type: 'SET_TRACK_HEIGHT'; trackId: TrackId; height: number }

Row height in pixels. The UI enforces a 36–140 range; the engine stores whatever value is dispatched.


SET_TRACK_NAME

{ type: 'SET_TRACK_NAME'; trackId: TrackId; name: string }

SET_TRACK_BLEND_MODE

{ type: 'SET_TRACK_BLEND_MODE'; trackId: TrackId; blendMode: string }

CSS blend mode string applied when compositing this track over tracks below it. Example: 'multiply', 'screen', 'overlay'.


SET_TRACK_OPACITY

{ type: 'SET_TRACK_OPACITY'; trackId: TrackId; opacity: number }

opacity must be in [0, 1]. The invariant checker rejects values outside this range with INVALID_OPACITY.


Asset Operations

REGISTER_ASSET

{ type: 'REGISTER_ASSET'; asset: Asset }

Adds an asset to assetRegistry. Must be dispatched before any INSERT_CLIP that references this assetId.

Rejects with: DUPLICATE_ID (if assetId already exists)

import { createAsset, frameRate, toFrame } from '@timelinx/core';

{
  type: 'REGISTER_ASSET',
  asset: createAsset({
    id: 'asset-interview',
    name: 'interview.mp4',
    mediaType: 'video',
    filePath: '/media/interview.mp4',
    intrinsicDuration: toFrame(9000),
    nativeFps: frameRate(30),
    sourceTimecodeOffset: toFrame(0),
  }),
}

UNREGISTER_ASSET

{ type: 'UNREGISTER_ASSET'; assetId: AssetId }

Removes an asset from assetRegistry.

Rejects with: NOT_FOUND, ASSET_IN_USE (if any clip in any track still references this assetId)


SET_ASSET_STATUS

{ type: 'SET_ASSET_STATUS'; assetId: AssetId; status: AssetStatus }

Updates the reachability status without unregistering. Use this when a file goes offline or a proxy becomes available.

status values: 'online' | 'offline' | 'proxy-only' | 'missing'


Timeline Operations

RENAME_TIMELINE

{ type: 'RENAME_TIMELINE'; name: string }

SET_TIMELINE_DURATION

{ type: 'SET_TIMELINE_DURATION'; duration: TimelineFrame }

Rejects with: OUT_OF_BOUNDS if any clip's timelineEnd would exceed the new duration.


SET_TIMELINE_START_TC

{ type: 'SET_TIMELINE_START_TC'; startTimecode: Timecode }

Sets the ruler's display origin. '00:00:00:00' means the ruler starts at midnight. '01:00:00:00' is common for broadcast deliverables.


SET_SEQUENCE_SETTINGS

{ type: 'SET_SEQUENCE_SETTINGS'; settings: Partial<SequenceSettings> }

Updates resolution, pixel aspect ratio, audio sample rate, or field order. Only the fields present in settings are updated.


Marker Operations

ADD_MARKER

{ type: 'ADD_MARKER'; marker: Marker }

Marker is a discriminated union:

// Point marker  - single frame
{ type: 'point', id, frame, label, color, scope, linkedClipId }

// Range marker  - frame range
{ type: 'range', id, frameStart, frameEnd, label, color, scope, linkedClipId }

scope values: 'global' | 'personal' | 'export'

linkedClipId - when set, this marker moves with the referenced clip during ripple operations.


MOVE_MARKER

{ type: 'MOVE_MARKER'; markerId: MarkerId; newFrame: TimelineFrame }

For range markers, moves the start frame and preserves the duration.


DELETE_MARKER

{ type: 'DELETE_MARKER'; markerId: MarkerId }

SET_IN_POINT

{ type: 'SET_IN_POINT'; frame: TimelineFrame | null }

Sets the export/loop region start. null clears it. The UI shows this as the left handle on the ruler's range overlay.


SET_OUT_POINT

{ type: 'SET_OUT_POINT'; frame: TimelineFrame | null }

ADD_BEAT_GRID

{
  type: 'ADD_BEAT_GRID';
  beatGrid: {
    readonly bpm: number;
    readonly timeSignature: readonly [number, number];
    readonly offset: TimelineFrame;
  }
}

Attaches a beat grid to the timeline for music-aligned editing. The snap index generates BeatGrid snap points from this. Only one beat grid is allowed per timeline.

Rejects with: BEAT_GRID_EXISTS


REMOVE_BEAT_GRID

{ type: 'REMOVE_BEAT_GRID' }

Generator Operations

INSERT_GENERATOR

{ type: 'INSERT_GENERATOR'; generator: Generator; trackId: TrackId; atFrame: TimelineFrame }

Creates a synthetic clip from a generator definition. Generators produce content without a source file - color bars, tone generators, countdown leaders. The engine creates the asset and clip together.

{
  type: 'INSERT_GENERATOR',
  trackId: toTrackId('v1'),
  atFrame: toFrame(0),
  generator: {
    type: 'color-bars',
    duration: toFrame(300),
  },
}

Caption Operations

Captions are subtitle/title items on subtitle and title tracks.

ADD_CAPTION

{
  type: 'ADD_CAPTION';
  caption: Omit<Caption, 'style'> & { style?: CaptionStyle };
  trackId: TrackId;
}

EDIT_CAPTION

{
  type: 'EDIT_CAPTION';
  captionId: CaptionId;
  trackId: TrackId;
  text?: string;
  language?: string;
  style?: Partial<CaptionStyle>;
  burnIn?: boolean;
  startFrame?: TimelineFrame;
  endFrame?: TimelineFrame;
}

Only the provided fields are updated.


DELETE_CAPTION

{ type: 'DELETE_CAPTION'; captionId: CaptionId; trackId: TrackId }

Effect & Keyframe Operations

Effects are compositing operations applied to individual clips. Each clip carries an ordered effects array. Keyframes animate an effect's parameters over time.

ADD_EFFECT

{ type: 'ADD_EFFECT'; clipId: ClipId; effect: Effect }

Appends an effect to the clip's effects array. Effect id must be unique within the clip.

Rejects with: CLIP_NOT_FOUND, DUPLICATE_EFFECT_ID


REMOVE_EFFECT

{ type: 'REMOVE_EFFECT'; clipId: ClipId; effectId: EffectId }

REORDER_EFFECT

{ type: 'REORDER_EFFECT'; clipId: ClipId; effectId: EffectId; newIndex: number }

The order of effects in the array determines the compositing pipeline order.

Rejects with: EFFECT_INDEX_OUT_OF_RANGE


SET_EFFECT_ENABLED

{ type: 'SET_EFFECT_ENABLED'; clipId: ClipId; effectId: EffectId; enabled: boolean }

Disabled effects are excluded from compositing but remain in the array.


SET_EFFECT_PARAM

{
  type: 'SET_EFFECT_PARAM';
  clipId: ClipId;
  effectId: EffectId;
  key: string;
  value: number | string | boolean;
}

Updates a single named parameter on an effect. The key must be a valid parameter name for the effect type.


ADD_KEYFRAME

{
  type: 'ADD_KEYFRAME';
  clipId: ClipId;
  effectId: EffectId;
  keyframe: Keyframe;
}

Keyframe shape: { id: KeyframeId, frame: TimelineFrame, value: number, easing: EasingCurve }

Keyframes must be unique by id within the effect and sorted ascending by frame.

Rejects with: EFFECT_NOT_FOUND, DUPLICATE_KEYFRAME_ID


MOVE_KEYFRAME

{ type: 'MOVE_KEYFRAME'; clipId: ClipId; effectId: EffectId; keyframeId: KeyframeId; newFrame: TimelineFrame }

Moving a keyframe out of order relative to other keyframes in the same effect rejects with KEYFRAME_ORDER_VIOLATION.


DELETE_KEYFRAME

{ type: 'DELETE_KEYFRAME'; clipId: ClipId; effectId: EffectId; keyframeId: KeyframeId }

SET_KEYFRAME_EASING

{ type: 'SET_KEYFRAME_EASING'; clipId: ClipId; effectId: EffectId; keyframeId: KeyframeId; easing: EasingCurve }

EasingCurve presets: LINEAR_EASING, HOLD_EASING. Custom bezier curves are represented as { type: 'cubic-bezier', cx1, cy1, cx2, cy2 }.


Creative Metadata Operations

SET_CLIP_TRANSFORM

{ type: 'SET_CLIP_TRANSFORM'; clipId: ClipId; transform: Partial<ClipTransform> }

ClipTransform fields:

FieldTypeDefaultNotes
xnumber0Horizontal position offset in pixels
ynumber0Vertical position offset
scaleXnumber1.0Horizontal scale factor
scaleYnumber1.0Vertical scale factor
rotationnumber0Degrees clockwise
anchorXnumber0.5Horizontal anchor (0=left, 1=right)
anchorYnumber0.5Vertical anchor (0=top, 1=bottom)
cropLeftnumber0Crop fraction 0–1
cropRightnumber0
cropTopnumber0
cropBottomnumber0

Only the fields present in Partial<ClipTransform> are updated.


SET_AUDIO_PROPERTIES

{ type: 'SET_AUDIO_PROPERTIES'; clipId: ClipId; properties: Partial<AudioProperties> }

AudioProperties fields:

FieldTypeDefaultNotes
volumenumber1.0Linear gain. 1.0 = 0 dB.
pannumber0-1.0 = full left, 1.0 = full right
channelRoutingChannelRoutingstereoChannel mapping for surround

ADD_TRANSITION

{ type: 'ADD_TRANSITION'; clipId: ClipId; transition: Transition }

Attaches a transition to a clip. Only one transition per clip is allowed. The transition spans the clip's cut point with its neighbors.

Transition fields:

  • id: TransitionId
  • type: TransitionType - e.g. 'dissolve' | 'wipe' | 'push'
  • durationFrames: number
  • alignment: TransitionAlignment - 'start' | 'end' | 'center'
  • params: Record<string, number | string> - type-specific parameters

Rejects with: already has a transition (use DELETE_TRANSITION first)


DELETE_TRANSITION

{ type: 'DELETE_TRANSITION'; clipId: ClipId }

Rejects with: TRANSITION_NOT_FOUND


SET_TRANSITION_DURATION

{ type: 'SET_TRANSITION_DURATION'; clipId: ClipId; durationFrames: number }

SET_TRANSITION_ALIGNMENT

{ type: 'SET_TRANSITION_ALIGNMENT'; clipId: ClipId; alignment: TransitionAlignment }

TransitionAlignment: 'start' | 'end' | 'center'

  • 'start' - transition starts at the clip's cut point
  • 'end' - transition ends at the cut point
  • 'center' - transition straddles the cut point equally

{ type: 'LINK_CLIPS'; linkGroup: LinkGroup }

LinkGroup: { id: LinkGroupId, clipIds: readonly ClipId[] }

Linked clips move together during MOVE_CLIP operations. Useful for keeping audio and video clips in sync.

Rejects with: DUPLICATE_LINK_GROUP_ID


{ type: 'UNLINK_CLIPS'; linkGroupId: LinkGroupId }

Rejects with: LINK_GROUP_NOT_FOUND


ADD_TRACK_GROUP

{ type: 'ADD_TRACK_GROUP'; trackGroup: TrackGroup }

TrackGroup: { id: TrackGroupId, name: string, trackIds: readonly TrackId[], collapsed?: boolean }

Creates a collapsible group of tracks. Tracks reference the group via track.groupId.

Rejects with: DUPLICATE_TRACK_GROUP_ID


DELETE_TRACK_GROUP

{ type: 'DELETE_TRACK_GROUP'; trackGroupId: TrackGroupId }

Removes the group. Tracks that referenced it have their groupId cleared.

Rejects with: TRACK_GROUP_NOT_FOUND


Rejection Reasons

Every rejected DispatchResult carries one of these reason values:

ReasonWhen it's returned
OVERLAPThe proposed clip position would overlap an existing clip on the same track
LOCKED_TRACKThe target track has locked: true
ASSET_MISSINGINSERT_CLIP references an assetId not in assetRegistry
TYPE_MISMATCHClip's mediaType does not match the target track's type
OUT_OF_BOUNDSClip would extend past timeline.duration
MEDIA_BOUNDS_INVALIDmediaOut would exceed asset.intrinsicDuration, or mediaOut ≤ mediaIn
ASSET_IN_USEUNREGISTER_ASSET on an asset still referenced by clips
TRACK_NOT_EMPTYDELETE_TRACK on a track with clips (must delete clips first)
SPEED_INVALIDspeed ≤ 0
INVARIANT_VIOLATEDPost-apply invariant check failed (whole-state check)
NOT_FOUNDGeneric: entity ID does not exist in state
BEAT_GRID_EXISTSADD_BEAT_GRID when one is already present
CLIP_NOT_FOUNDClip ID is not found in any track
DUPLICATE_EFFECT_IDADD_EFFECT with an ID already on the clip
EFFECT_NOT_FOUNDEffect ID is missing from the clip
EFFECT_INDEX_OUT_OF_RANGEREORDER_EFFECT target index is out of bounds
KEYFRAME_NOT_FOUNDKeyframe ID is missing from the effect
DUPLICATE_KEYFRAME_IDADD_KEYFRAME with an ID already in the effect
INVALID_RANGEA start frame ≥ end frame, or atFrame outside clip bounds
TRANSITION_NOT_FOUNDDELETE_TRANSITION or SET_TRANSITION_* on a clip with no transition
LINK_GROUP_NOT_FOUNDUNLINK_CLIPS with an unknown group ID
TRACK_GROUP_NOT_FOUNDDELETE_TRACK_GROUP with an unknown group ID
DUPLICATE_LINK_GROUP_IDLINK_CLIPS with an ID already in linkGroups
DUPLICATE_TRACK_GROUP_IDADD_TRACK_GROUP with an ID already in trackGroups
INVALID_OPACITYSET_TRACK_OPACITY value outside 0–1
TRACK_NOT_FOUNDTrack ID not found
UNKNOWN_OPERATIONThe type field does not match any known operation
DUPLICATE_IDInserting any entity whose ID already exists

Handling rejections:

const result = dispatch(state, transaction);

if (!result.accepted) {
  switch (result.reason) {
    case 'OVERLAP':
      showSnapHint('Clip overlaps an existing clip. Use ripple insert or move the target.');
      break;
    case 'ASSET_MISSING':
      showError('Asset not found. Re-link the source file and try again.');
      break;
    case 'LOCKED_TRACK':
      // silently ignore  - user tried to edit a locked track
      break;
    default:
      console.error('[dispatch]', result.reason, result.message);
  }
}

On this page