Miraiclip SDK
API reference

@miraiclip/editor

Headless editor logic for Miraiclip: timeline scale, clip drag and trim with snapping, selectors, typed commands, gestures, shortcuts, media import. No UI framework.

npm install @miraiclip/editor
Version0.2.0
Runs inAnywhere (a few media helpers need the browser)
Exports62
GuidesHeadless editor logic, Timeline

Functions

beginClipDrag

Start dragging a clip (move, or trim either edge) with snapping. Pure: the caller turns pointer events into update(deltaUs, overTrackId) and calls commit() on release, so a drag is one undo step. Returns null when the clip doesn't exist or sits on a locked track.

function beginClipDrag(project: Project, clipId: string, mode: ClipDragMode, options: ClipDragOptions): ClipDrag | null

Guides: Timeline, Timeline, Headless editor logic

beginGesture

A live, continuous edit (canvas drag, rotate) that lands in history as ONE entry. Each update undoes the gesture's previous entry and commits the new absolute state, so the canvas shows every intermediate frame while undo steps back over the whole gesture at once.

Safety: it only ever undoes an entry it committed itself. If anything else commits, undoes or redoes mid-gesture (an AI edit, a keyboard undo), the next update starts a fresh entry instead of undoing someone else's.

function beginGesture(project: Project, label: string): Gesture

Guides: Headless editor logic

beginTransformDrag

Start moving, scaling or rotating a clip from bounds (where it is drawn now, keyframes evaluated). start is the pointer in composition px. Returns null when the clip can't be transformed (missing, audio, locked).

Moves snap the content's box (edges and center) to the frame's edges and center; scale is uniform around the origin; rotation snaps gently to right angles, or to 15° steps with Shift.

function beginTransformDrag(project: Project, bounds: TransformBounds, mode: TransformDragMode, start: {
    x: number;
    y: number;
}, options?: TransformDragOptions): TransformDrag | null

Guides: Selection & canvas editing, Transform box, Headless editor logic

canTransformClip

True when the clip can be moved on the preview: it exists, is visual, and its track isn't locked.

function canTransformClip(project: Project, clipId: string): boolean

Guides: Headless editor logic

clipsByTrack

Every track's clips, sorted by start time. Cached per doc.clips object.

function clipsByTrack(clips: ProjectDocument["clips"]): Map<string, Clip[]>

Guides: Headless editor logic

clipsInRange

Clips overlapping [fromUs, toUs), from a start-sorted list (one track from clipsByTrack). A clip starting before the range can still reach into it, so the search looks back past the longest clip on the list. Use it to mount only the clips a timeline lane shows.

function clipsInRange(clips: readonly Clip[], fromUs: Us, toUs: Us): Clip[]

Guides: Timeline, Headless editor logic

clipSnapTargets

Default snap targets for a clip: 0, the playhead, and the other clips' edges on its track.

function clipSnapTargets(project: Project, clip: Clip): Us[]

compositionDurationUs

End of the last clip: the composition's length. Cached per doc.clips object.

function compositionDurationUs(clips: ProjectDocument["clips"]): Us

Guides: Headless editor logic

cornerCursor

CSS resize cursor for corner index (0 top-left, clockwise), adjusted for rotation.

function cornerCursor(index: number, rotationDeg: number): string

Guides: Headless editor logic

createCommands

Typed command builders. UI code dispatches through these helpers instead of hand-writing raw command objects, so the command vocabulary lives in one place. Property edits are clip/set-property and track/set-property; there is no generic clip/update.

function createCommands(project: Project): {
    setProjectSettings(patch: {
        width?: number;
        height?: number;
        fps?: number;
        name?: string;
    }): void;
    addAsset(payload: {
        id: string;
        kind: "video" | "audio" | "image" | "font";
        src: string;
        durationUs?: Us;
        width?: number;
        height?: number;
        fps?: number;
        family?: string;
        weight?: FontWeight;
        style?: FontStyle;
        weightRange?: [FontWeight, FontWeight];
        name?: string;
        source?: AssetSource;
        license?: AssetLicense;
        attribution?: string;
    }): void;
    setAssetProperty(id: string, patch: {
        name?: string | null;
        source?: AssetSource | null;
        license?: AssetLicense | null;
        attribution?: string | null;
    }): void;
    addTrack(payload: {
        id: string;
        kind: "video" | "audio";
        name?: string;
        index?: number;
    }): void;
    removeTrack(trackId: string): void;
    renameTrack(trackId: string, name: string): void;
    reorderTrack(trackId: string, index: number): void;
    setTrackProperty(trackId: string, patch: {
        muted?: boolean;
        solo?: boolean;
        locked?: boolean;
        hidden?: boolean;
    }): void;
    addVideoClip(payload: {
        id: string;
        trackId: string;
        assetId: string;
        startUs: Us;
        durationUs: Us;
        trimStartUs?: Us;
        volume?: number;
        fadeInUs?: Us;
        fadeOutUs?: Us;
        transform?: Partial<Transform>;
    }): void;
    addAudioClip(payload: {
        id: string;
        trackId: string;
        assetId: string;
        startUs: Us;
        durationUs: Us;
        trimStartUs?: Us;
        volume?: number;
        fadeInUs?: Us;
        fadeOutUs?: Us;
    }): void;
    addImageClip(payload: {
        id: string;
        trackId: string;
        assetId: string;
        startUs: Us;
        durationUs: Us;
        transform?: Partial<Transform>;
    }): void;
    addTextClip(payload: {
        id: string;
        trackId: string;
        startUs: Us;
        durationUs: Us;
        text: string;
        fontFamily?: string;
        fontSizePx?: number;
        color?: string;
        fontWeight?: FontWeight;
        textAlign?: TextAlign;
        transform?: Partial<Transform>;
    }): void;
    addCaptionClip(payload: {
        id: string;
        trackId: string;
        startUs: Us;
        durationUs: Us;
        words: CaptionWord[];
        style?: Partial<CaptionStyle>;
        transform?: Partial<Transform>;
    }): void;
    addHtmlClip(payload: {
        id: string;
        trackId: string;
        startUs: Us;
        durationUs: Us;
        template: string;
        params: Record<string, HtmlParamValue>;
        widthPx?: number;
        heightPx?: number;
        animated?: boolean;
        transform?: Partial<Transform>;
    }): void;
    moveClip(clipId: string, patch: {
        startUs?: Us;
        trackId?: string;
    }): void;
    trimClip(clipId: string, patch: {
        startUs?: Us;
        durationUs?: Us;
        trimStartUs?: Us;
    }): void;
    splitClip(clipId: string, atUs: Us, newClipId?: string): void;
    duplicateClip(clipId: string, patch?: {
        newClipId?: string;
        startUs?: Us;
        trackId?: string;
    }): void;
    removeClip(clipId: string): void;
    setClipProperty(clipId: string, patch: {
        transform?: Partial<Transform>;
        volume?: number;
        fadeInUs?: Us | null;
        fadeOutUs?: Us | null;
        text?: string;
        fontFamily?: string;
        fontSizePx?: number;
        color?: string;
        fontWeight?: FontWeight | null;
        fontStyle?: FontStyle | null;
        lineHeight?: number | null;
        letterSpacing?: number | null;
        textAlign?: TextAlign | null;
        style?: Record<string, unknown>;
        params?: Record<string, HtmlParamValue>;
        template?: string;
        unsetParams?: string[];
        widthPx?: number | null;
        heightPx?: number | null;
        animated?: boolean;
        words?: CaptionWord[];
    }): void;
    addEffect(clipId: string, kind: string, params?: Record<string, unknown>, enabled?: boolean): void;
    updateEffect(clipId: string, effectId: string, patch: {
        params?: Record<string, unknown>;
        enabled?: boolean;
    }): void;
    removeEffect(clipId: string, effectId: string): void;
    addTransition(payload: {
        id?: string;
        kind: string;
        fromClipId: string;
        toClipId: string;
        durationUs: Us;
        params?: Record<string, unknown>;
    }): void;
    updateTransition(transitionId: string, patch: {
        durationUs?: Us;
        params?: Record<string, unknown>;
    }): void;
    removeTransition(transitionId: string): void;
    setKeyframe(payload: {
        clipId: string;
        property: AnimatableProperty;
        timeUs: Us;
        value: number;
        easing?: EasingPreset | Easing;
        anchor?: "start" | "end";
    }): void;
    clearKeyframes(clipId: string, property?: AnimatableProperty): void;
    select(ids: string[]): void;
    setPlayhead(us: Us): void;
    transaction(fn: () => void, label?: string): void;
}

Guides: Headless editor logic

fillScale

The renderer draws video/image clips so that transform.scale = 1 FITS the composition (contain). FILL (cover) is therefore max ratio / min ratio. Needs the asset's pixel size; returns undefined when it isn't known.

function fillScale(asset: Asset | undefined, settings: Pick<ProjectSettings, "width" | "height">): number | undefined

Guides: Headless editor logic

followPlayheadScroll

Page-follow during playback: when the playhead nears the right edge (or is behind the left one), the scroll that puts it 10% in from the left. Returns null when no scroll is needed.

function followPlayheadScroll(input: PlayheadViewInput & {
    edgePx?: number;
}): number | null

Guides: Headless editor logic

formatClock

mm:ss (the compact clock used in transport bars and ruler labels).

function formatClock(us: Us): string

Guides: Timeline, Headless editor logic

isFramable

Clips whose picture can be framed (fit/fill/scale).

function isFramable(clip: Clip | undefined): clip is Clip & {
    assetId: string;
}

isSourceReachable

Whether a media src can still be fetched: catches session-only blob: URLs in a reopened project and URLs that 404. data: URLs are always reachable.

function isSourceReachable(src: string): Promise<boolean>

Guides: Headless editor logic, Compatibility, Roadmap

isTrackFree

True when [startUs, endUs) overlaps no clip on the track, except ignoreClipId (the clip being moved).

function isTrackFree(doc: ProjectDocument, trackId: string, startUs: Us, endUs: Us, ignoreClipId?: string): boolean

Guides: Headless editor logic

lowerBound

First index in start-sorted clips whose start is at or after us.

function lowerBound(clips: readonly Clip[], us: Us): number

matchEditorShortcut

The default key map:

  • Delete / Backspace → delete the selected clips
  • S → split the selected clips at the playhead
  • Ctrl/⌘ + Z → undo; Ctrl/⌘ + Shift + Z or Ctrl/⌘ + Y → redo

Returns null for any other key. Callers skip events from text fields.

function matchEditorShortcut(event: ShortcutKey): EditorAction | null

Guides: Headless editor logic

newId

A unique id with a readable prefix, e.g. clip-V1StGXR8_Z.

function newId(prefix?: string): string

nudgeClip

Move a clip by whole composition pixels (arrow-key nudges), as one undo step. Returns false when it can't move.

function nudgeClip(project: Project, bounds: TransformBounds, dxPx: number, dyPx: number): boolean

Guides: Headless editor logic

placeAssetAtPlayhead

Add an asset at the playhead on the first accepting, unlocked track that is free for the clip's span; when none is, add a new track after the last accepting one. One undo step either way. Returns the clip id or null.

function placeAssetAtPlayhead(project: Project, assetId: string, options?: PlaceAssetOptions): string | null

Guides: Asset import, Blocks, Headless editor logic

placeAssetOnTrack

Add an asset as a clip on a track (clip/add). Returns the clip id, or null when the asset or track is missing, the track is locked, the track can't hold that kind, or the asset isn't placeable (fonts).

function placeAssetOnTrack(project: Project, assetId: string, trackId: string, startUs: Us, options?: PlaceAssetOptions): string | null

Guides: Headless editor logic

playheadOffscreenSide

Which side of the viewport the playhead is off, or null when it is visible.

function playheadOffscreenSide(input: PlayheadViewInput): "left" | "right" | null

Guides: Headless editor logic

presetFor

function presetFor(settings: Pick<ProjectSettings, "width" | "height">): AspectPreset | undefined

probeMediaFile

Read a media file's metadata (and a thumbnail for video/image) in the browser, without decoding it into the engine. Returns everything needed for an asset/add command, with src as an object URL (revoke it when the asset is gone, or upload the file and use your own URL). Best-effort: on a probe failure it still returns a usable asset with a fallback duration. Returns null for files that aren't video, audio or images.

function probeMediaFile(file: File): Promise<ProbedMedia | null>

Guides: Asset import, Blocks, Headless editor logic, Quickstart

pxToUs

Pixels → microseconds at a zoom. Not rounded: round (or snapToFrame) before dispatching.

function pxToUs(px: number, pxPerSecond: number): Us

Guides: Timeline, Headless editor logic

refillForAspect

For an aspect-ratio change: clips currently framed to FILL stay filled (their fill scale changes with the aspect). Returns [clipId, newScale] pairs.

function refillForAspect(doc: ProjectDocument, next: Pick<ProjectSettings, "width" | "height">): [string, number][]

Guides: Headless editor logic

revealPlayheadScroll

The scroll that brings the playhead into view, 30% in from the left.

function revealPlayheadScroll(input: Omit<PlayheadViewInput, "scrollLeftPx">): number

Guides: Headless editor logic

rotateHandlePosition

Where a rotate handle sits: distancePx beyond the middle of the top edge, along the content's "up". Returns the handle and the edge midpoint (for a connecting line), in composition px.

function rotateHandlePosition(bounds: TransformBounds, distancePx: number): {
    handle: CompositionPoint;
    anchor: CompositionPoint;
}

Guides: Headless editor logic

runEditorAction

Run an editing action against the project. Delete and split act on every selected clip on an unlocked track, as one undo step. Returns whether anything happened (so a key handler knows to preventDefault).

function runEditorAction(project: Project, action: EditorAction): boolean

Guides: Timeline, Headless editor logic

snapOffset

Snap the closest candidate to the closest target within thresholdPx. Returns the offset to add and the target hit, or null.

function snapOffset(candidates: readonly number[], targets: readonly number[], thresholdPx: number): {
    delta: number;
    target: number;
} | null

Guides: Headless editor logic

snapToTargets

The value in targets closest to valueUs within thresholdUs, else valueUs itself.

function snapToTargets(valueUs: Us, targets: readonly Us[], thresholdUs: Us): Us

trackCuts

The cuts on one track, in time order, from its start-sorted clips (one list from clipsByTrack) and its transitions. Only visual clips form cuts, as in core's findCuts, which covers the whole project with the transition-length limits.

function trackCuts(clips: readonly Clip[], transitions?: readonly Transition[]): TrackCut[]

Guides: Timeline, Headless editor logic

transitionsByTrack

Every track's transitions. Cached per doc.transitions object.

function transitionsByTrack(transitions: ProjectDocument["transitions"]): Map<string, Transition[]>

Guides: Headless editor logic

usToPx

Microseconds → pixels at a zoom.

function usToPx(us: Us, pxPerSecond: number): number

Guides: Headless editor logic

visibleRange

The time range a timeline should render (virtualisation): what is visible plus one viewport of overscan on each side, snapped to half-viewport steps so ordinary scrolling changes the range only every half screen. Clips outside it need not be in the DOM at all.

function visibleRange({ scrollLeftPx, viewportWidthPx, pxPerSecond }: VisibleRangeInput): {
    fromUs: Us;
    toUs: Us;
}

Guides: Timeline, Headless editor logic

wheelZoomFactor

Zoom factor for a wheel event's deltaY (smooth on trackpads, steady on mice).

function wheelZoomFactor(deltaY: number): number

Guides: Headless editor logic

writeClipTransform

Write transform values. A property with keyframes gets a keyframe at the playhead; the others change the clip's static transform.

function writeClipTransform(project: Project, clipId: string, patch: Partial<Transform>): void

Guides: Transform box, Headless editor logic

zoomAtPoint

Zoom around a point: the new zoom and the scroll that keeps the anchored time under the pointer.

function zoomAtPoint(input: ZoomAtPointInput): {
    pxPerSecond: number;
    scrollLeftPx: number;
}

Guides: Timeline, Headless editor logic

Constants

ASPECT_PRESETS

Output-format presets. The short side stays 1080 so quality is constant.

const ASPECT_PRESETS: readonly [{
    readonly id: "9:16";
    readonly label: "9:16";
    readonly hint: "Reels, Shorts, TikTok";
    readonly width: 1080;
    readonly height: 1920;
}, {
    readonly id: "16:9";
    readonly label: "16:9";
    readonly hint: "YouTube, landscape";
    readonly width: 1920;
    readonly height: 1080;
}, {
    readonly id: "1:1";
    readonly label: "1:1";
    readonly hint: "Square post";
    readonly width: 1080;
    readonly height: 1080;
}, {
    readonly id: "4:5";
    readonly label: "4:5";
    readonly hint: "Instagram feed";
    readonly width: 1080;
    readonly height: 135 …

Guides: Headless editor logic

Interfaces

ClipDrag

MemberTypeDescription
previewClipDragPreview
update(deltaUs: Us, overTrackId?: string) => ClipDragPreviewPointer moved: deltaUs is the horizontal movement since the drag began; overTrackId the track under the pointer (move only). A track that is locked or can't hold this clip kind is ignored.
commit() => booleanDispatch the result as one clip/move or clip/trim. Returns false when nothing changed.

ClipDragOptions

MemberTypeDescription
snapThresholdUsUsSnap distance in µs. Callers usually derive it from a pixel distance at the current zoom: pxToUs(8, pxPerSecond). 0 turns snapping off.
minDurationUs?UsShortest a trim can make the clip (default 200 ms).
snapTargets?readonly Us[]Snap targets in µs. Default: 0, the playhead, and every other clip edge on the clip's track.

ClipDragPreview

The dragged clip as it would land right now. Nothing is dispatched until commit.

MemberTypeDescription
clipIdstring
modeClipDragMode
originTrackIdstring
trackIdstringThe track it would land on (differs from originTrackId only when moving across tracks).
startUsUs
durationUsUs

Guides: Timeline

CompositionPoint

A point in composition pixels.

MemberTypeDescription
xPxnumber
yPxnumber

Gesture

MemberTypeDescription
update(apply: () => void) => voidApply the gesture's CURRENT absolute state (e.g. "x = start + delta"). Replaces the previous update in history.
end() => voidFinish: the last update stays as one history entry.

PlaceAssetOptions

MemberTypeDescription
clipId?stringId for the new clip (default: a generated clip-… id).
imageDurationUs?UsLength of a still image clip (default 5 s).
select?booleanSelect the new clip (default true).

PlayheadViewInput

MemberTypeDescription
playheadUsUs
pxPerSecondnumber
scrollLeftPxnumber
viewportWidthPxnumber

ProbedMedia

MemberTypeDescription
kindExtract<AssetKind, "video" | "audio" | "image">
srcstring
durationUsnumber
width?number
height?number
thumb?stringdata: URL thumbnail (video first frame / the image itself); undefined for audio.

ShortcutKey

The parts of a keyboard event the shortcut map reads (a DOM KeyboardEvent fits).

MemberTypeDescription
keystring
ctrlKey?boolean
metaKey?boolean
shiftKey?boolean
altKey?boolean

TrackCut

A cut on one track: from ends exactly where to starts.

MemberTypeDescription
fromClip
toClip
atUsUsTimeline position of the cut.
transition?TransitionThe transition on this cut, if any.

TransformBounds

Where a clip is drawn. Structurally the renderer's ClipBounds (getClipBounds / hitTest on the preview), so those can be passed as is.

MemberTypeDescription
clipIdstring
originXPxnumberThe placement point (transform x/y in pixels). Scale and rotation pivot here.
originYPxnumber
centerXPxnumberCenter of the drawn content.
centerYPxnumber
scalenumber
rotationDegnumber
cornersreadonly [CompositionPoint, CompositionPoint, CompositionPoint, CompositionPoint]Corners clockwise from top-left, rotation applied.

TransformDrag

MemberTypeDescription
clipIdstring
modeTransformDragMode
movedbooleanTrue once the pointer passed the drag threshold.
update(point: { x: number; y: number; }, modifiers?: TransformModifiers) => TransformGuidesPointer moved to point (composition px). Applies the edit live and returns the guides to show.
end() => voidFinish: the drag is one undo step.

TransformDragOptions

MemberTypeDescription
snapThresholdPx?numberSnap distance for moves, in composition px (0, the default, turns snapping off).
dragThresholdPx?numberMovement below this (composition px) is a click, not a drag (default 0).
minScale?numberScale limits (default 0.05 … 20).
maxScale?number

TransformGuides

Frame lines a move snapped to, in composition px, for drawing guides.

MemberTypeDescription
xnumber[]
ynumber[]

TransformModifiers

MemberTypeDescription
altKey?booleanMove: place freely, without snapping.
shiftKey?booleanRotate: 15° steps.

VisibleRangeInput

MemberTypeDescription
scrollLeftPxnumberHorizontal scroll of the lanes, in px.
viewportWidthPxnumberWidth of the lanes' viewport, in px (without a sticky header column).
pxPerSecondnumber

ZoomAtPointInput

MemberTypeDescription
pxPerSecondnumber
scrollLeftPxnumber
anchorPxnumberPointer x within the lanes' viewport, in px. The time under it stays put.
factornumberMultiplier on the current zoom (> 1 zooms in).
minPxPerSecondnumber
maxPxPerSecondnumber

Types

AspectPreset

type AspectPreset = (typeof ASPECT_PRESETS)[number];

ClipDragMode

What a drag changes: the whole clip, its in-point, or its out-point.

type ClipDragMode = "move" | "trim-start" | "trim-end";

Commands

type Commands = ReturnType<typeof createCommands>;

EditorAction

Editing actions with a default key.

type EditorAction = "delete" | "split" | "undo" | "redo";

TransformDragMode

type TransformDragMode = "move" | "scale" | "rotate";

On this page