Code

Examples

Integration examples for common use cases. Copy, adapt, ship.

Integration

Timeline Only

The timeline as a drop-in component — no renderer, no decode pipeline. Ideal for building a custom rendering layer or exploring the edit model.

timeline-only.tsx
1import { useRef } from 'react'2import {3  EditorProvider,4  Timeline,5  useTimelineEngine,6  usePlaybackStore,7  framesToTimecode,8  type TimelineRef,9} from '@elah/editor'1011const TRACKS = [12  { kind: 'video' as const, name: 'Video / Image' },13  { kind: 'audio' as const, name: 'Audio' },14  { kind: 'text' as const, name: 'Text' },15]1617function Transport({ fps }: { fps: number }) {18  const { isPlaying, togglePlayPause, currentFrame } =19    usePlaybackStore((s) => s)2021  return (22    <div style={{ display: 'flex', gap: 8, padding: 8 }}>23      <button onClick={togglePlayPause}>24        {isPlaying ? '⏸' : '▶'}25      </button>26      <span style={{ fontFamily: 'monospace', fontSize: 12 }}>27        {framesToTimecode(currentFrame, fps)}28      </span>29    </div>30  )31}3233export default function TimelineOnlyExample() {34  const ref = useRef<TimelineRef>(null)35  const fps = 303637  return (38    <EditorProvider fps={fps} initialTracks={TRACKS}>39      <Transport fps={fps} />40      <Timeline ref={ref} fps={fps} style={{ height: 260 }} />41    </EditorProvider>42  )43}
Advanced

Custom Demuxer

Replace mediabunny with your own demuxer by implementing the DemuxerFactory interface.

custom-demuxer.tsx
1import {2  EditorProvider,3  Preview,4  Timeline,5  type DemuxerFactory,6} from '@elah/editor'78// Implement DemuxerFactory with your own backend9const myDemuxerFactory: DemuxerFactory = () => ({10  async probe(src: string) {11    const res = await fetch(`/api/probe?src=${encodeURIComponent(src)}`)12    return res.json() // { width, height, durationSeconds, fps }13  },14  async demux(src, opts, onChunk) {15    const res = await fetch(src)16    const reader = res.body!.getReader()17    // Feed chunks to your WebCodecs VideoDecoder18    while (true) {19      const { done, value } = await reader.read()20      if (done) break21      onChunk(new EncodedVideoChunk({22        type: 'key',23        timestamp: opts.startTimestamp,24        data: value,25      }))26    }27  },28  destroy() {},29})3031export default function CustomDemuxerExample() {32  return (33    <EditorProvider fps={30}>34      <Preview demuxerFactory={myDemuxerFactory} style={{ flex: 1 }} />35      <Timeline fps={30} style={{ height: 240 }} />36    </EditorProvider>37  )38}
Export

Export with Progress UI

Full export flow with progress bar, cancel support, and MP4 download trigger.

export-with-progress.tsx
1import { useState, useCallback } from 'react'2import {3  exportVideo,4  useTimelineEngine,5  createDefaultDemuxerFactory,6  type ExportProgress,7} from '@elah/editor'89const demuxerFactory = createDefaultDemuxerFactory()1011export function ExportPanel() {12  const engine = useTimelineEngine()13  const [progress, setProgress] = useState<ExportProgress | null>(null)14  const [error, setError] = useState<string | null>(null)1516  const handleExport = useCallback(async () => {17    setError(null)18    try {19      const blob = await exportVideo(engine.getProject(), {20        fps: 30,21        demuxerFactory,22        videoCodec: 'avc',23        videoBitrate: 8_000_000,24        onProgress: setProgress,25      })2627      // Trigger download28      const a = Object.assign(document.createElement('a'), {29        href: URL.createObjectURL(blob),30        download: 'export.mp4',31      })32      a.click()33      URL.revokeObjectURL(a.href)34    } catch (e) {35      setError(e instanceof Error ? e.message : 'Export failed')36    } finally {37      setProgress(null)38    }39  }, [engine])4041  return (42    <div style={{ padding: 16 }}>43      <button44        onClick={handleExport}45        disabled={!!progress}46        style={{ padding: '8px 16px', background: '#b7102a', color: '#fff' }}47      >48        {progress ? `Exporting ${Math.round(progress.percent)}%` : 'Export MP4'}49      </button>5051      {progress && (52        <div style={{ marginTop: 8 }}>53          <progress value={progress.percent} max={100} style={{ width: '100%' }} />54          <div style={{ fontSize: 11, color: '#666', marginTop: 4 }}>55            {progress.phase} · frame {progress.frame} / {progress.totalFrames}56          </div>57        </div>58      )}5960      {error && (61        <div style={{ marginTop: 8, color: '#b7102a', fontSize: 12 }}>62          Error: {error}63        </div>64      )}65    </div>66  )67}
Advanced

Custom Renderer with resolveTimeline

Use the pure resolver to build a completely custom rendering layer — DOM, Canvas 2D, or any other target.

resolve-timeline-custom.tsx
1import { useEffect, useRef } from 'react'2import {3  EditorProvider,4  Timeline,5  useTimelineEngine,6  usePlaybackStore,7  useTracksStore,8  resolveTimeline,9} from '@elah/editor'1011// DOM renderer: updates div positions instead of WebGL12function DomPreview() {13  const engine = useTimelineEngine()14  const currentFrame = usePlaybackStore((s) => s.currentFrame)15  const containerRef = useRef<HTMLDivElement>(null)1617  useEffect(() => {18    const project = engine.getProject()19    const scene = resolveTimeline(currentFrame, project)2021    if (!containerRef.current) return22    containerRef.current.innerHTML = ''2324    // Render text clips as DOM elements25    for (const text of scene.texts) {26      const el = document.createElement('div')27      el.textContent = text.text?.content ?? ''28      el.style.cssText = `29        position: absolute;30        opacity: ${text.opacity};31        font-size: ${text.text?.fontSize ?? 32}px;32        color: ${text.text?.color ?? '#fff'};33        left: 50%;34        top: 50%;35        transform: translate(-50%, -50%);36      `37      containerRef.current.appendChild(el)38    }39  }, [currentFrame, engine])4041  return (42    <div43      ref={containerRef}44      style={{45        position: 'relative',46        flex: 1,47        background: '#000',48        overflow: 'hidden',49      }}50    />51  )52}5354export default function CustomRendererExample() {55  return (56    <EditorProvider fps={30}>57      <div style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>58        <DomPreview />59        <Timeline fps={30} style={{ height: 240 }} />60      </div>61    </EditorProvider>62  )63}
Need something more specific?

Check the API reference or open an issue on GitHub.