Build a Timeline Library
Like shadcn/ui
- - A comprehensive, step-by-step blueprint for building a production-ready, framework-agnostic timeline library with CLI installation, extensible components, and video editor capabilities โ from first principles to shipped product. -
-Introduction: What You're Building
-- You're not building a video editor. You're building the foundation that enables a thousand video editors, audio editors, animation tools, and timeline-based applications to exist. This is a library, not an application. -
- -The Core Concept
-Think of your timeline library as three distinct layers:
- -You're following the shadcn/ui philosophy: copy, don't install. Components aren't distributed as npm packages โ they're source code files that get copied into the user's project. This gives users 100% control while you provide the starting point.
-What Makes This Different
-Traditional libraries (like Timeline.js or Vis.js) are monolithic: they bundle everything together. You install one package and get opinionated UI, styling, and logic all mixed together. This makes customization painful.
- -Your approach is modular and composable:
--
-
- Core logic is separate โ Users can build their own UI on top -
- Components are owned by users โ They modify source code, not fight with props -
- Framework adapters are thin โ Easy to support React, Vue, Svelte, Solid -
- No opinions on styling โ Users bring their own CSS/Tailwind/etc. -
- - - - - -
Vision & Goals
-- Let's crystalize exactly what you're building and why. This keeps you focused when you inevitably face 1000 implementation choices. -
- -Primary Goal
-Build a timeline component library that developers can install with a single CLI command and customize completely. The timeline should be flexible enough to power:
--
-
- Video editors (DaVinci Resolve, Premiere) -
- Audio editors (Audacity, GarageBand) -
- Animation tools (After Effects, Rive) -
- Project management tools (Gantt charts) -
- Music production (FL Studio, Ableton) -
Secondary Goal (Future)
-Eventually expand into a full video editing library by adding:
--
-
- Video/audio playback synchronization -
- Real-time preview rendering -
- Effects pipeline (transitions, filters, transforms) -
- Export/encoding capabilities -
Do NOT build the video editor features first. Build an excellent timeline library first. Get that right. Make it successful. The video editor features can be added later as optional packages (@timeline/playback, @timeline/effects, @timeline/export).
-If you start building everything at once, you'll burn out and ship nothing.
-Success Metrics
-Extreme Flexibility
-Users can customize every visual aspect and behavior. No "this is how it works" โ only "here's a starting point."
-Performance First
-Handle 1000+ clips without lag. Virtualization, efficient re-renders, optimized calculations.
-Developer Experience
-Install in 30 seconds. Great TypeScript support. Excellent docs. Feels like magic.
-Framework Agnostic
-Core logic works anywhere. Adapters for React, Vue, Svelte. Easy to add new frameworks.
-- - - - - -
Architecture Deep Dive
-- This is the most important section. Get the architecture right and everything else flows naturally. Get it wrong and you'll rewrite everything later. -
- -The Three-Layer Architecture (Current Implementation)
-The timeline library is built on three distinct layers with clear separation of concerns:
- -1. Core Layer (@timeline/core) - The Engine
-The core is a deterministic, frame-based timeline editing kernel with:
--
-
- TimelineEngine โ Main API with 41 public methods for all operations -
- 8 Internal Systems โ Validation, queries, snapping, linking, grouping, clipboard, drag-state, asset-registry -
- Immutable state โ All operations return new state, never mutate -
- Built-in undo/redo โ History system with configurable limits -
- Frame-based time โ Deterministic, no floating-point errors -
- Zero framework dependencies โ Pure TypeScript -
// The TimelineEngine is the main public API -import { TimelineEngine, createTimelineState } from '@timeline/core' - -// Create engine with initial state -const engine = new TimelineEngine(createTimelineState({ - timeline: createTimeline({ /* ... */ }) -})) - -// All operations go through the engine -engine.addTrack(track) // Returns DispatchResult -engine.addClip(trackId, clip) // Validates and updates state -engine.moveClip(clipId, newStart) // Supports undo/redo -engine.undo() // Revert last operation -engine.redo() // Reapply operation - -// Subscribe to state changes -const unsubscribe = engine.subscribe((state) => { - console.log('State changed:', state) -})-
Public API (from @timeline/core): TimelineEngine, factory functions, types, frame utilities
Internal API (from @timeline/core/internal): Low-level operations, systems, utilities โ used by tests and advanced integrations
2. Adapter Layer (@timeline/react) - Framework Integration
-The React adapter provides hooks and context for state management:
--
-
- TimelineProvider โ Context provider wrapping the engine -
- useEngine() โ Access the engine instance -
- useTimeline() โ Subscribe to full timeline state -
- useTrack(trackId) โ Subscribe to specific track -
- useClip(clipId) โ Subscribe to specific clip -
- Optimized subscriptions โ Listeners receive state directly -
import { TimelineProvider, useTimeline, useEngine } from '@timeline/react' - -// Wrap app with provider -function App() { - return ( - <TimelineProvider engine={engine}> - <TimelineView /> - </TimelineProvider> - ) -} - -// Use hooks in components -function TimelineView() { - const { state } = useTimeline() // Full state - const engine = useEngine() // Engine instance - const track = useTrack('track-1') // Specific track - - return <div>{/* UI */}</div> -}-
3. UI Layer (@timeline/ui) - Components
-Pre-built components with local state management for UI interactions:
--
-
- Timeline โ Main component managing playhead, zoom, snapping, selection -
- Track โ Track component with mute/solo/lock controls -
- Clip โ Draggable, resizable clip with selection -
- TimeRuler โ Time ruler with click-to-seek -
- Local UI state โ Managed in Timeline component, not in engine -
- Keyboard shortcuts โ Full support for all operations -
// Timeline manages local UI state -export function Timeline() { - const { state } = useTimeline() - const engine = useEngine() - - // Local UI state (not in engine) - const [playhead, setPlayhead] = useState(frame(0)) - const [pixelsPerFrame, setPixelsPerFrame] = useState(1) - const [snappingEnabled, setSnappingEnabled] = useState(true) - const [editingMode, setEditingMode] = useState('normal') - const [selectedClipIds, setSelectedClipIds] = useState(new Set()) - - // Keyboard shortcuts - useEffect(() => { - const handleKeyDown = (e) => { - if (e.key === 'Delete') { - selectedClipIds.forEach(id => engine.removeClip(id)) - } - // ... more shortcuts - } - document.addEventListener('keydown', handleKeyDown) - }, [selectedClipIds]) - - return <div>{/* Render timeline */}</div> -}-
Clear separation: Engine handles timeline data and operations. React adapter manages subscriptions. UI components handle user interactions and visual state.
-Flexible UI state: Playhead, zoom, and selection live in UI (not engine) because they're view-specific and shouldn't be in undo/redo history.
-Testable: Core engine is 100% testable without React. UI components can be tested with React Testing Library.
-- - - - - -
How Timeline Works: First Principles
-- Before writing code, you need to deeply understand what a timeline actually is and how it behaves. Let's build this mental model from the ground up. -
- -The Fundamental Concept
-A timeline is a 2D coordinate system where:
--
-
- X-axis = Time (measured in seconds, frames, or milliseconds) -
- Y-axis = Tracks (layers of content stacked vertically) -
Every piece of content (video clip, audio clip, image, text) is a rectangle in this 2D space:
--
-
- X position = when it starts (e.g., 5.2 seconds) -
- Width = how long it plays (e.g., 3.0 seconds) -
- Y position = which track it's on (e.g., track 2) -
Core Timeline Behaviors
-Users interact with timelines in specific ways. You need to support all of these:
- -1. Playback Control
--
-
- Play/Pause โ Move playhead forward at constant speed -
- Scrubbing โ Drag playhead to jump to specific time -
- Step Forward/Back โ Move playhead by 1 frame -
- Jump to Edit Points โ Snap playhead to clip boundaries -
2. Clip Manipulation
--
-
- Move โ Drag clip horizontally (change start time) -
- Move to Track โ Drag clip vertically (change layer) -
- Trim Start โ Drag left edge to change in-point -
- Trim End โ Drag right edge to change out-point -
- Split โ Cut clip at playhead position into two clips -
- Delete โ Remove clip entirely -
- Duplicate โ Copy clip to new position -
3. Track Management
--
-
- Add Track โ Create new layer -
- Delete Track โ Remove layer (what happens to clips?) -
- Reorder Tracks โ Change stacking order -
- Lock/Unlock โ Prevent accidental edits -
- Mute/Solo โ Control audio/video visibility -
4. Zoom & Pan
--
-
- Zoom In/Out โ Change time scale (pixels per second) -
- Pan Left/Right โ Scroll through time -
- Fit to View โ Zoom to show entire timeline -
- Zoom to Selection โ Focus on selected clips -
The Math Behind It
-Every timeline operation is just math. Here are the key calculations:
- -Time to Pixels
-// Given: time in seconds, zoom level (pixels per second) -function timeToPixels(time: number, zoom: number): number { - return time * zoom -} - -// Example: 5 seconds at 100px/sec = 500px from timeline start -const x = timeToPixels(5, 100) // 500-
Pixels to Time
-// Inverse operation: convert mouse X position to time -function pixelsToTime(pixels: number, zoom: number): number { - return pixels / zoom -} - -// Example: Clicked at 750px with 100px/sec zoom = 7.5 seconds -const time = pixelsToTime(750, 100) // 7.5-
Snap to Grid
-// Snap time to nearest grid interval (e.g., 0.1 second increments) -function snapToGrid(time: number, gridSize: number): number { - return Math.round(time / gridSize) * gridSize -} - -// Example: 5.23 seconds snaps to 5.2 with 0.1 grid -const snapped = snapToGrid(5.23, 0.1) // 5.2-
Clip Collision Detection
-// Do two clips overlap in time? -function clipsOverlap(clip1: Clip, clip2: Clip): boolean { - const end1 = clip1.start + clip1.duration - const end2 = clip2.start + clip2.duration - - // Check if they're on same track AND time ranges overlap - return clip1.trackId === clip2.trackId && - clip1.start < end2 && - clip2.start < end1 -}-
Once you understand that timeline editing is just moving rectangles in 2D space with constraints, the entire problem becomes much simpler. All the complex UI interactions (drag, resize, snap) are just math operations on rectangle coordinates.
-- - - - - -
Data Model: Your Timeline Schema
-- The data model is the single source of truth for your entire library. Get this right and everything flows naturally. Get it wrong and you'll constantly fight your own architecture. -
- -Core Type Definitions
-Here's the complete type system for your timeline library:
- -// โโโ TIMELINE โโโ -export interface Timeline { - id: string - tracks: Track[] - duration: number // Total timeline duration in seconds - frameRate: number // FPS (e.g., 30, 60) - sampleRate?: number // For audio (e.g., 44100, 48000) - metadata?: Record<string, any> -} - -// โโโ TRACK โโโ -export interface Track { - id: string - name: string - type: 'video' | 'audio' | 'subtitle' | 'custom' - clips: Clip[] - height: number // Track height in pixels - locked: boolean // Prevent editing - visible: boolean // Show/hide track - muted: boolean // Mute audio - order: number // Stack order (0 = bottom) - color?: string // Visual theme color -} - -// โโโ CLIP โโโ -export interface Clip { - id: string - trackId: string // Which track owns this clip - name: string - type: 'video' | 'audio' | 'image' | 'text' | 'custom' - - // Timing - start: number // Start time on timeline (seconds) - duration: number // How long clip plays (seconds) - - // Source trimming (for video/audio files) - trimStart: number // Where to start in source file - trimEnd: number // Where to end in source file - - // Source reference - sourceUrl?: string // URL to media file - sourceId?: string // Reference to media asset - - // Effects and properties - effects: Effect[] - properties: ClipProperties - - // Visual - color?: string - thumbnail?: string // Preview image URL -} - -// โโโ CLIP PROPERTIES โโโ -export interface ClipProperties { - opacity: number // 0-1 - volume: number // 0-1 (for audio) - speed: number // Playback speed multiplier - position: { x: number; y: number } - scale: { x: number; y: number } - rotation: number // Degrees - keyframes: Keyframe[] -} - -// โโโ KEYFRAME โโโ -export interface Keyframe { - id: string - time: number // Relative to clip start - property: string // e.g., 'opacity', 'position.x' - value: number | { x: number; y: number } - easing: 'linear' | 'easeIn' | 'easeOut' | 'easeInOut' -} - -// โโโ EFFECT โโโ -export interface Effect { - id: string - type: string // 'blur', 'brightness', 'fadeIn', etc. - enabled: boolean - parameters: Record<string, any> -}-
Design Principles
-Immutable
-All data structures are readonly. Functions return new objects, never mutate inputs. This makes undo/redo trivial.
-ID-Based References
-Clips reference tracks by ID, not by index. This makes reordering tracks safe and prevents stale references.
-Extensible
-Custom metadata fields on every type. Users can store their own data without modifying your types.
-Normalized
-Flat structure where possible. Tracks contain clip IDs, not nested clip objects. Easier to query and update.
-You might be tempted to add 50 more fields "just in case." Don't. Start minimal. Add fields only when you have a concrete use case. You can always add fields later (breaking change), but removing them is painful.
-The schema above is everything you need for a production timeline library.
-- - - - - -
State Management Strategy
-- How you manage timeline state determines the entire developer experience. This is where most timeline libraries fail. Here's how to do it right. -
- -The Core Principle: Unidirectional Data Flow
-Your timeline state flows in one direction:
-User Action (drag clip) - โ -Core Function (moveClip) - โ -New Timeline State - โ -React Re-render - โ -Updated UI-
Immutability Pattern
-Every state update creates a NEW timeline object:
- -// โ CORRECT: Pure function returns new object -function moveClip(timeline: Timeline, clipId, newStart): Timeline { - const newTracks = timeline.tracks.map(track => ({ - ...track, - clips: track.clips.map(clip => - clip.id === clipId ? { ...clip, start: newStart } : clip - ) - })) - - return { ...timeline, tracks: newTracks } -} - -// โ WRONG: Mutates input directly -function moveClip(timeline, clipId, newStart) { - const clip = timeline.tracks[0].clips[0] - clip.start = newStart // BAD! Mutation! - return timeline -}-
Undo/Redo Implementation
-With immutable state, undo/redo is trivial:
- -export function useHistory<T>(initialState: T) { - const [past, setPast] = useState<T[]>([]) - const [present, setPresent] = useState(initialState) - const [future, setFuture] = useState<T[]>([]) - - const push = (newState: T) => { - setPast([...past, present]) - setPresent(newState) - setFuture([]) // Clear future on new action - } - - const undo = () => { - if (past.length === 0) return - const previous = past[past.length - 1] - setPast(past.slice(0, -1)) - setFuture([present, ...future]) - setPresent(previous) - } - - const redo = () => { - if (future.length === 0) return - const next = future[0] - setPast([...past, present]) - setPresent(next) - setFuture(future.slice(1)) - } - - return { state: present, push, undo, redo, canUndo: past.length > 0, canRedo: future.length > 0 } -}-
That's it. ~30 lines for full undo/redo. Because state is immutable, you just swap timeline objects.
- -Performance Optimization
-Immutability can cause unnecessary re-renders. Here's how to optimize:
- -1. Memoization
-import { useMemo } from 'react' - -function Timeline({ timeline }) { - // Only recalculate when timeline.tracks changes - const sortedTracks = useMemo( - () => timeline.tracks.slice().sort((a, b) => a.order - b.order), - [timeline.tracks] - ) - - return <div>{/* render tracks */}</div> -}-
2. React.memo for Components
-const Clip = React.memo(({ clip, onMove }) => { - // Only re-renders when clip props actually change - return <div>{clip.name}</div> -})-
3. Virtualization for Large Timelines
-import { useVirtualizer } from '@tanstack/react-virtual' - -function Timeline({ timeline }) { - const rowVirtualizer = useVirtualizer({ - count: timeline.tracks.length, - getScrollElement: () => parentRef.current, - estimateSize: () => 64, // Track height - }) - - // Only render visible tracks - return rowVirtualizer.getVirtualItems().map(virtualRow => ( - <Track key={virtualRow.key} track={timeline.tracks[virtualRow.index]} /> - )) -}-
-
-
- Keep state minimal โ Store only source data, derive everything else -
- Normalize data โ Use IDs for references, not nested objects -
- Batch updates โ Update multiple things in one setState call -
- Profile before optimizing โ Don't add complexity until you measure a problem -
- - - - - -
Monorepo Setup
-- Your timeline library needs a monorepo structure to manage multiple packages (core, react, vue, cli, docs). Here's exactly how to set it up. -
- -Why Monorepo?
-A monorepo lets you:
--
-
- Share code easily โ All packages can import from @timeline/core -
- Version together โ Release all packages in sync -
- Test together โ Run all tests with one command -
- Develop faster โ No need to publish/install during development -
Recommended Tool: Turborepo
-Use Turborepo (by Vercel). It's what shadcn/ui uses, it's fast, and it handles caching beautifully.
- -Initial Setup
-# Create new project -npx create-turbo@latest timeline-library - -# Or manually: -mkdir timeline-library -cd timeline-library -npm init -y -npm install turbo --save-dev-
Folder Structure
-timeline-library/ -โโโ packages/ -โ โโโ core/ # @timeline/core -โ โ โโโ src/ -โ โ โ โโโ types.ts # All TypeScript types -โ โ โ โโโ mutations.ts # State update functions -โ โ โ โโโ queries.ts # Read-only query functions -โ โ โ โโโ utils.ts # Time conversion, snapping, etc. -โ โ โ โโโ index.ts # Public exports -โ โ โโโ tests/ -โ โ โโโ package.json -โ โ โโโ tsconfig.json -โ โ -โ โโโ react/ # @timeline/react -โ โ โโโ src/ -โ โ โ โโโ hooks/ -โ โ โ โ โโโ useTimeline.ts -โ โ โ โ โโโ useHistory.ts -โ โ โ โ โโโ useDrag.ts -โ โ โ โ โโโ usePlayback.ts -โ โ โ โ โโโ useZoom.ts -โ โ โ โโโ index.ts -โ โ โโโ tests/ -โ โ โโโ package.json -โ โ โโโ tsconfig.json -โ โ -โ โโโ vue/ # @timeline/vue (future) -โ โ โโโ ... -โ โ -โ โโโ cli/ # timeline-cli -โ โ โโโ src/ -โ โ โ โโโ commands/ -โ โ โ โ โโโ init.ts -โ โ โ โ โโโ add.ts -โ โ โ โโโ utils/ -โ โ โ โ โโโ registry.ts -โ โ โ โ โโโ installer.ts -โ โ โ โ โโโ framework-detector.ts -โ โ โ โโโ index.ts -โ โ โโโ package.json -โ โ โโโ tsconfig.json -โ โ -โ โโโ tsconfig/ # Shared TypeScript configs -โ โโโ base.json -โ โโโ react.json -โ โโโ node.json -โ -โโโ apps/ -โ โโโ docs/ # Documentation site -โ โ โโโ Next.js or Astro -โ โ โโโ ... -โ โ -โ โโโ playground/ # Demo/testing app -โ โโโ React app -โ โโโ ... -โ -โโโ registry/ # Component source files -โ โโโ components/ -โ โ โโโ timeline.tsx -โ โ โโโ track.tsx -โ โ โโโ clip.tsx -โ โ โโโ playhead.tsx -โ โ โโโ ruler.tsx -โ โโโ registry.json # Component metadata -โ -โโโ turbo.json # Turbo configuration -โโโ package.json # Root package.json -โโโ pnpm-workspace.yaml # PNPM workspace config-
Root package.json
-{
- "name": "timeline-library",
- "private": true,
- "scripts": {
- "build": "turbo run build",
- "dev": "turbo run dev",
- "test": "turbo run test",
- "lint": "turbo run lint",
- "format": "prettier --write \"**/*.{ts,tsx,md}\""
- },
- "devDependencies": {
- "turbo": "latest",
- "typescript": "^5.3.0",
- "prettier": "^3.1.0",
- "eslint": "^8.55.0"
- },
- "packageManager": "pnpm@8.0.0"
-}
- turbo.json Configuration
-{
- "$schema": "https://turbo.build/schema.json",
- "globalDependencies": ["**/.env.*local"],
- "pipeline": {
- "build": {
- "dependsOn": ["^build"],
- "outputs": ["dist/**", ".next/**", "!.next/cache/**"]
- },
- "test": {
- "dependsOn": ["build"],
- "outputs": ["coverage/**"]
- },
- "lint": {
- "outputs": []
- },
- "dev": {
- "cache": false,
- "persistent": true
- }
- }
-}
- Core Package Setup
-{
- "name": "@timeline/core",
- "version": "0.1.0",
- "main": "./dist/index.js",
- "module": "./dist/index.mjs",
- "types": "./dist/index.d.ts",
- "exports": {
- ".": {
- "import": "./dist/index.mjs",
- "require": "./dist/index.js",
- "types": "./dist/index.d.ts"
- }
- },
- "scripts": {
- "build": "tsup src/index.ts --format cjs,esm --dts",
- "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
- "test": "vitest",
- "test:ui": "vitest --ui"
- },
- "devDependencies": {
- "tsup": "^8.0.0",
- "vitest": "^1.0.0",
- "@timeline/tsconfig": "workspace:*"
- }
-}
- -
-
- pnpm โ Faster than npm, better disk space usage, strict dependency resolution -
- Turborepo โ Caches builds, runs tasks in parallel, handles dependencies -
- tsup โ Zero-config TypeScript bundler, outputs CJS + ESM + types -
- Vitest โ Fast, Vite-powered test runner with great DX -
- - - - - -
Technology Choices
-- Every technology choice has tradeoffs. Here's what to use and why, with alternatives explained. -
- -Core Technologies (Required)
- -| Technology | -Choice | -Why | -Alternatives | -
|---|---|---|---|
| Language | -TypeScript 5.3+ | -Type safety, great DX, industry standard | -JavaScript (no types = pain) | -
| Package Manager | -pnpm | -Fast, strict, disk-efficient | -npm (slower), yarn (okay) | -
| Monorepo | -Turborepo | -Fast builds, great caching, simple config | -Nx (more complex), Lerna (outdated) | -
| Bundler | -tsup | -Zero config, outputs CJS+ESM+types | -Rollup (more config), tsc (no bundling) | -
| Test Framework | -Vitest | -Fast, Vite-powered, great UI | -Jest (slower), uvu (minimal) | -
Framework Adapters
- -React (@timeline/react)
-{
- "name": "@timeline/react",
- "peerDependencies": {
- "react": "^18.0.0",
- "react-dom": "^18.0.0"
- },
- "dependencies": {
- "@timeline/core": "workspace:*"
- }
-}
- Vue (@timeline/vue) - Future
-{
- "name": "@timeline/vue",
- "peerDependencies": {
- "vue": "^3.3.0"
- },
- "dependencies": {
- "@timeline/core": "workspace:*"
- }
-}
- Component Development
- -| Concern | -Choice | -Reasoning | -
|---|---|---|
| Styling | -Tailwind CSS (optional) | -Utility-first, users can override easily, optional | -
| Primitives | -Radix UI patterns | -Accessible, composable, unstyled by default | -
| Drag & Drop | -Custom implementation | -Full control, no heavy deps, exact behavior needed | -
| Virtualization | -@tanstack/react-virtual | -Best-in-class, handles 1000s of items, framework-agnostic core | -
CLI Tool Stack
- -{
- "name": "timeline-cli",
- "bin": {
- "timeline": "./dist/index.js"
- },
- "dependencies": {
- "commander": "^11.1.0", // CLI framework
- "prompts": "^2.4.2", // Interactive prompts
- "chalk": "^5.3.0", // Colored output
- "ora": "^8.0.1", // Spinners
- "fs-extra": "^11.2.0", // File operations
- "execa": "^8.0.1" // Run shell commands
- }
-}
- Documentation Site
- -Next.js (Recommended)
-Pros: MDX support, App Router, great SEO, Vercel deployment
- Cons: Heavier, React-only
Astro
-Pros: Super fast, framework-agnostic, great DX
- Cons: Less mature, smaller ecosystem
VitePress
-Pros: Vite-powered, Vue-based, simple
- Cons: Less flexible than Next.js
For most developers:
--
-
- TypeScript + pnpm + Turborepo -
- Vitest for testing, tsup for building -
- Next.js for docs (MDX + App Router) -
- Tailwind CSS (optional) for component styling -
- Commander.js for CLI -
This is battle-tested, well-documented, and has great community support.
-- - - - - -
Framework Adapters
-- Adapters are the bridge between your pure core logic and framework-specific features. Here's how to build them for React, Vue, and beyond. -
- -Adapter Design Pattern
-Every adapter follows the same pattern:
--
-
- Import core functions from @timeline/core -
- Wrap in framework primitives (hooks, composables, stores) -
- Handle reactivity so UI updates automatically -
- Manage side effects (playback, animations, etc.) -
React Adapter
- -useTimeline Hook
-import { useState, useCallback } from 'react' -import { - Timeline, - moveClip as coreMoveClip, - trimClip as coreTrimClip, - splitClip as coreSplitClip, - addTrack as coreAddTrack, - deleteClip as coreDeleteClip -} from '@timeline/core' - -export function useTimeline(initialTimeline: Timeline) { - const [timeline, setTimeline] = useState(initialTimeline) - - // Wrap core functions with state updates - const moveClip = useCallback( - (clipId: string, newStart: number, newTrackId?: string) => { - setTimeline(prev => coreMoveClip(prev, clipId, newStart, newTrackId)) - }, - [] - ) - - const trimClip = useCallback( - (clipId: string, newStart?: number, newDuration?: number) => { - setTimeline(prev => coreTrimClip(prev, clipId, newStart, newDuration)) - }, - [] - ) - - const splitClip = useCallback( - (clipId: string, splitTime: number) => { - setTimeline(prev => coreSplitClip(prev, clipId, splitTime)) - }, - [] - ) - - const addTrack = useCallback((type: 'video' | 'audio') => { - setTimeline(prev => coreAddTrack(prev, type)) - }, []) - - const deleteClip = useCallback((clipId: string) => { - setTimeline(prev => coreDeleteClip(prev, clipId)) - }, []) - - return { - timeline, - actions: { - moveClip, - trimClip, - splitClip, - addTrack, - deleteClip - } - } -}-
useHistory Hook (Undo/Redo)
-import { useState, useCallback } from 'react' - -export function useHistory<T>(initialState: T) { - const [past, setPast] = useState<T[]>([]) - const [present, setPresent] = useState(initialState) - const [future, setFuture] = useState<T[]>([]) - - const push = useCallback((newState: T) => { - setPast(p => [...p, present]) - setPresent(newState) - setFuture([]) // Clear redo stack - }, [present]) - - const undo = useCallback(() => { - if (past.length === 0) return - - const previous = past[past.length - 1] - const newPast = past.slice(0, -1) - - setPast(newPast) - setFuture([present, ...future]) - setPresent(previous) - }, [past, present, future]) - - const redo = useCallback(() => { - if (future.length === 0) return - - const next = future[0] - const newFuture = future.slice(1) - - setPast([...past, present]) - setPresent(next) - setFuture(newFuture) - }, [past, present, future]) - - const reset = useCallback(() => { - setPast([]) - setFuture([]) - }, []) - - return { - state: present, - push, - undo, - redo, - reset, - canUndo: past.length > 0, - canRedo: future.length > 0 - } -}-
usePlayback Hook
-import { useState, useEffect, useCallback, useRef } from 'react' - -export function usePlayback(frameRate = 30) { - const [currentTime, setCurrentTime] = useState(0) - const [isPlaying, setIsPlaying] = useState(false) - const animationFrameRef = useRef<number>() - const lastTimeRef = useRef(Date.now()) - - // Playback loop - useEffect(() => { - if (!isPlaying) return - - const frameTime = 1000 / frameRate // milliseconds per frame - - const tick = () => { - const now = Date.now() - const delta = now - lastTimeRef.current - - if (delta >= frameTime) { - setCurrentTime(t => t + (delta / 1000)) - lastTimeRef.current = now - } - - animationFrameRef.current = requestAnimationFrame(tick) - } - - animationFrameRef.current = requestAnimationFrame(tick) - - return () => { - if (animationFrameRef.current) { - cancelAnimationFrame(animationFrameRef.current) - } - } - }, [isPlaying, frameRate]) - - const play = useCallback(() => { - lastTimeRef.current = Date.now() - setIsPlaying(true) - }, []) - - const pause = useCallback(() => { - setIsPlaying(false) - }, []) - - const togglePlay = useCallback(() => { - setIsPlaying(p => !p) - }, []) - - const seek = useCallback((time: number) => { - setCurrentTime(time) - lastTimeRef.current = Date.now() - }, []) - - const stepForward = useCallback(() => { - setCurrentTime(t => t + (1 / frameRate)) - }, [frameRate]) - - const stepBackward = useCallback(() => { - setCurrentTime(t => Math.max(0, t - (1 / frameRate))) - }, [frameRate]) - - return { - currentTime, - isPlaying, - play, - pause, - togglePlay, - seek, - stepForward, - stepBackward - } -}-
Vue Adapter (Composables)
-Vue uses composables instead of hooks. Here's the equivalent useTimeline:
- -import { ref, readonly } from 'vue' -import { - Timeline, - moveClip as coreMoveClip, - trimClip as coreTrimClip -} from '@timeline/core' - -export function useTimeline(initialTimeline: Timeline) { - const timeline = ref(initialTimeline) - - const moveClip = (clipId: string, newStart: number, newTrackId?: string) => { - timeline.value = coreMoveClip(timeline.value, clipId, newStart, newTrackId) - } - - const trimClip = (clipId: string, newStart?: number, newDuration?: number) => { - timeline.value = coreTrimClip(timeline.value, clipId, newStart, newDuration) - } - - return { - timeline: readonly(timeline), - actions: { - moveClip, - trimClip - } - } -}-
Notice how both React and Vue adapters:
--
-
- Import the same core functions -
- Wrap them in framework-specific state primitives -
- Return the same API shape -
This means adding support for Svelte, Solid, or any framework is just writing a thin wrapper. The core logic never changes.
-- - - - - -
Core Package Implementation
-- The core package is where all the magic happens. Pure functions, zero dependencies, 100% tested. This is the foundation everything else builds on. -
- -File Structure
-src/ -โโโ types.ts # All TypeScript interfaces -โโโ mutations.ts # State-changing functions (moveClip, addTrack, etc.) -โโโ queries.ts # Read-only functions (getActiveClips, etc.) -โโโ utils.ts # Helper functions (timeToPixels, snapToGrid, etc.) -โโโ keyframes.ts # Keyframe interpolation math -โโโ index.ts # Public exports-
Complete mutations.ts
-import { Timeline, Track, Clip } from './types' - -// โโโ CLIP MUTATIONS โโโ - -export function moveClip( - timeline: Timeline, - clipId: string, - newStart: number, - newTrackId?: string -): Timeline { - let movedClip: Clip | null = null - - // Remove clip from old track - const tracksWithoutClip = timeline.tracks.map(track => { - const clip = track.clips.find(c => c.id === clipId) - if (clip) movedClip = clip - - return { - ...track, - clips: track.clips.filter(c => c.id !== clipId) - } - }) - - if (!movedClip) return timeline // Clip not found - - // Add clip to new track with updated position - const targetTrackId = newTrackId || movedClip.trackId - const updatedClip = { ...movedClip, start: newStart, trackId: targetTrackId } - - const finalTracks = tracksWithoutClip.map(track => - track.id === targetTrackId - ? { ...track, clips: [...track.clips, updatedClip].sort((a, b) => a.start - b.start) } - : track - ) - - return { ...timeline, tracks: finalTracks } -} - -export function trimClip( - timeline: Timeline, - clipId: string, - newStart?: number, - newDuration?: number -): Timeline { - const tracks = timeline.tracks.map(track => ({ - ...track, - clips: track.clips.map(clip => - clip.id === clipId - ? { - ...clip, - ...(newStart !== undefined && { start: newStart }), - ...(newDuration !== undefined && { duration: newDuration }) - } - : clip - ) - })) - - return { ...timeline, tracks } -} - -export function splitClip( - timeline: Timeline, - clipId: string, - splitTime: number -): Timeline { - let splitOccurred = false - - const tracks = timeline.tracks.map(track => { - const clip = track.clips.find(c => c.id === clipId) - if (!clip) return track - - // Check if split point is within clip bounds - if (splitTime <= clip.start || splitTime >= clip.start + clip.duration) { - return track - } - - splitOccurred = true - - // Create two new clips - const timeIntoClip = splitTime - clip.start - - const leftClip: Clip = { - ...clip, - id: `${clip.id}-left`, - duration: timeIntoClip, - trimEnd: clip.trimStart + timeIntoClip - } - - const rightClip: Clip = { - ...clip, - id: `${clip.id}-right`, - start: splitTime, - duration: clip.duration - timeIntoClip, - trimStart: clip.trimStart + timeIntoClip - } - - // Replace original clip with two new clips - const newClips = track.clips - .filter(c => c.id !== clipId) - .concat([leftClip, rightClip]) - .sort((a, b) => a.start - b.start) - - return { ...track, clips: newClips } - }) - - return splitOccurred ? { ...timeline, tracks } : timeline -} - -export function deleteClip( - timeline: Timeline, - clipId: string -): Timeline { - const tracks = timeline.tracks.map(track => ({ - ...track, - clips: track.clips.filter(c => c.id !== clipId) - })) - - return { ...timeline, tracks } -} - -// โโโ TRACK MUTATIONS โโโ - -export function addTrack( - timeline: Timeline, - type: 'video' | 'audio' | 'subtitle' = 'video' -): Timeline { - const newTrack: Track = { - id: `track-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, - name: `${type.charAt(0).toUpperCase() + type.slice(1)} Track ${timeline.tracks.length + 1}`, - type, - clips: [], - height: 64, - locked: false, - visible: true, - muted: false, - order: timeline.tracks.length - } - - return { - ...timeline, - tracks: [...timeline.tracks, newTrack] - } -} - -export function deleteTrack( - timeline: Timeline, - trackId: string -): Timeline { - const tracks = timeline.tracks - .filter(t => t.id !== trackId) - .map((track, index) => ({ ...track, order: index })) - - return { ...timeline, tracks } -} - -export function reorderTracks( - timeline: Timeline, - trackId: string, - newOrder: number -): Timeline { - const trackIndex = timeline.tracks.findIndex(t => t.id === trackId) - if (trackIndex === -1) return timeline - - const tracks = [...timeline.tracks] - const [movedTrack] = tracks.splice(trackIndex, 1) - tracks.splice(newOrder, 0, movedTrack) - - // Update order values - const reorderedTracks = tracks.map((track, index) => ({ - ...track, - order: index - })) - - return { ...timeline, tracks: reorderedTracks } -}-
Complete queries.ts
-import { Timeline, Clip, Track } from './types' - -/** - * Get all clips that are active (playing) at a specific time - */ -export function getActiveClipsAtTime( - timeline: Timeline, - time: number -): Clip[] { - const activeClips: Clip[] = [] - - for (const track of timeline.tracks) { - for (const clip of track.clips) { - const clipEnd = clip.start + clip.duration - if (clip.start <= time && time < clipEnd) { - activeClips.push(clip) - } - } - } - - // Sort by track order (bottom to top) - return activeClips.sort((a, b) => { - const trackA = timeline.tracks.find(t => t.id === a.trackId) - const trackB = timeline.tracks.find(t => t.id === b.trackId) - return (trackA?.order ?? 0) - (trackB?.order ?? 0) - }) -} - -/** - * Get the next edit point (clip boundary) after current time - */ -export function getNextEditPoint( - timeline: Timeline, - currentTime: number -): number | null { - let nextPoint = Infinity - - for (const track of timeline.tracks) { - for (const clip of track.clips) { - // Check clip start - if (clip.start > currentTime && clip.start < nextPoint) { - nextPoint = clip.start - } - // Check clip end - const clipEnd = clip.start + clip.duration - if (clipEnd > currentTime && clipEnd < nextPoint) { - nextPoint = clipEnd - } - } - } - - return nextPoint === Infinity ? null : nextPoint -} - -/** - * Get the previous edit point before current time - */ -export function getPreviousEditPoint( - timeline: Timeline, - currentTime: number -): number | null { - let prevPoint = -Infinity - - for (const track of timeline.tracks) { - for (const clip of track.clips) { - if (clip.start < currentTime && clip.start > prevPoint) { - prevPoint = clip.start - } - const clipEnd = clip.start + clip.duration - if (clipEnd < currentTime && clipEnd > prevPoint) { - prevPoint = clipEnd - } - } - } - - return prevPoint === -Infinity ? null : prevPoint -} - -/** - * Check if two clips overlap in time - */ -export function clipsOverlap(clip1: Clip, clip2: Clip): boolean { - // Only clips on same track can overlap - if (clip1.trackId !== clip2.trackId) return false - - const end1 = clip1.start + clip1.duration - const end2 = clip2.start + clip2.duration - - return clip1.start < end2 && clip2.start < end1 -} - -/** - * Get all clips on a specific track - */ -export function getClipsByTrack( - timeline: Timeline, - trackId: string -): Clip[] { - const track = timeline.tracks.find(t => t.id === trackId) - return track ? track.clips : [] -}-
-
-
- Pure functions only โ No side effects, always return new objects -
- Comprehensive JSDoc โ Every public function documented -
- Defensive programming โ Check for null/undefined, return original if invalid -
- 100% test coverage โ Every function has unit tests -
- - - - - - - -
React Package Deep Dive
-- The React adapter package brings core timeline functionality into React applications with hooks, context, and optimal re-rendering strategies. -
- -useDrag Hook (Complex Interaction)
-Dragging clips is the most complex interaction in a timeline. Here's a production-ready implementation:
- -import { useState, useCallback, useRef, useEffect } from 'react' - -interface DragState { - isDragging: boolean - clipId: string | null - startX: number - startY: number - currentX: number - currentY: number - originalStart: number - originalTrackId: string -} - -export function useDrag(pixelsPerSecond: number = 100) { - const [dragState, setDragState] = useState<DragState>({ - isDragging: false, - clipId: null, - startX: 0, - startY: 0, - currentX: 0, - currentY: 0, - originalStart: 0, - originalTrackId: '' - }) - - const dragRef = useRef(null) - - const startDrag = useCallback(( - clipId: string, - mouseX: number, - mouseY: number, - clipStart: number, - trackId: string - ) => { - setDragState({ - isDragging: true, - clipId, - startX: mouseX, - startY: mouseY, - currentX: mouseX, - currentY: mouseY, - originalStart: clipStart, - originalTrackId: trackId - }) - }, []) - - const stopDrag = useCallback(() => { - setDragState(prev => ({ ...prev, isDragging: false, clipId: null })) - }, []) - - // Mouse move handler - useEffect(() => { - if (!dragState.isDragging) return - - const handleMouseMove = (e: MouseEvent) => { - setDragState(prev => ({ - ...prev, - currentX: e.clientX, - currentY: e.clientY - })) - } - - const handleMouseUp = () => { - stopDrag() - } - - window.addEventListener('mousemove', handleMouseMove) - window.addEventListener('mouseup', handleMouseUp) - - return () => { - window.removeEventListener('mousemove', handleMouseMove) - window.removeEventListener('mouseup', handleMouseUp) - } - }, [dragState.isDragging, stopDrag]) - - // Calculate new time based on drag distance - const deltaX = dragState.currentX - dragState.startX - const deltaTime = deltaX / pixelsPerSecond - const newStart = Math.max(0, dragState.originalStart + deltaTime) - - return { - dragState, - startDrag, - stopDrag, - newStart, - deltaX, - deltaY: dragState.currentY - dragState.startY - } -}-
useZoom Hook
-import { useState, useCallback } from 'react' - -export function useZoom( - initialZoom = 100, - minZoom = 10, - maxZoom = 1000 -) { - const [zoom, setZoom] = useState(initialZoom) - const [scrollLeft, setScrollLeft] = useState(0) - - const zoomIn = useCallback((factor = 1.2) => { - setZoom(z => Math.min(maxZoom, z * factor)) - }, [maxZoom]) - - const zoomOut = useCallback((factor = 1.2) => { - setZoom(z => Math.max(minZoom, z / factor)) - }, [minZoom]) - - const zoomTo = useCallback((newZoom: number) => { - setZoom(Math.max(minZoom, Math.min(maxZoom, newZoom))) - }, [minZoom, maxZoom]) - - const fitToView = useCallback((timelineDuration: number, containerWidth: number) => { - const newZoom = containerWidth / timelineDuration - zoomTo(newZoom) - setScrollLeft(0) - }, [zoomTo]) - - return { - zoom, - scrollLeft, - setScrollLeft, - zoomIn, - zoomOut, - zoomTo, - fitToView, - pixelsPerSecond: zoom - } -}-
TimelineContext (Advanced)
-For complex apps, provide timeline state through context:
- -import { createContext, useContext, ReactNode } from 'react' -import { Timeline } from '@timeline/core' -import { useTimeline } from '../hooks/useTimeline' -import { useHistory } from '../hooks/useHistory' -import { usePlayback } from '../hooks/usePlayback' -import { useZoom } from '../hooks/useZoom' - -interface TimelineContextValue { - timeline: Timeline - actions: ReturnType<typeof useTimeline>['actions'] - history: ReturnType<typeof useHistory> - playback: ReturnType<typeof usePlayback> - zoom: ReturnType<typeof useZoom> -} - -const TimelineContext = createContext<TimelineContextValue | null>(null) - -export function TimelineProvider({ - children, - initialTimeline -}: { - children: ReactNode - initialTimeline: Timeline -}) { - const { timeline, actions } = useTimeline(initialTimeline) - const history = useHistory(timeline) - const playback = usePlayback(timeline.frameRate) - const zoom = useZoom() - - return ( - <TimelineContext.Provider - value={{ timeline, actions, history, playback, zoom }} - > - {children} - </TimelineContext.Provider> - ) -} - -export function useTimelineContext() { - const context = useContext(TimelineContext) - if (!context) { - throw new Error('useTimelineContext must be used within TimelineProvider') - } - return context -}-
- - - - - -
UI Components
-- These are the actual timeline components users install via CLI. They're unstyled primitives that users fully own and customize. -
- -Timeline Component (Root)
-import { useTimelineContext } from '@timeline/react' -import { Track } from './track' -import { Playhead } from './playhead' -import { TimeRuler } from './ruler' - -export function Timeline() { - const { timeline, zoom } = useTimelineContext() - - return ( - <div className="timeline-container"> - {/* Time ruler at top */} - <TimeRuler - duration={timeline.duration} - zoom={zoom.pixelsPerSecond} - /> - - {/* Scrollable tracks area */} - <div className="timeline-tracks"> - {timeline.tracks - .sort((a, b) => a.order - b.order) - .map(track => ( - <Track key={track.id} track={track} /> - ))} - </div> - - {/* Playhead overlay */} - <Playhead /> - </div> - ) -}-
Clip Component (Draggable)
-import { useTimelineContext } from '@timeline/react' -import { useDrag } from '@timeline/react' -import { Clip as ClipType } from '@timeline/core' - -interface ClipProps { - clip: ClipType -} - -export function Clip({ clip }: ClipProps) { - const { actions, zoom } = useTimelineContext() - const drag = useDrag(zoom.pixelsPerSecond) - - const handleMouseDown = (e: React.MouseEvent) => { - e.preventDefault() - drag.startDrag(clip.id, e.clientX, e.clientY, clip.start, clip.trackId) - } - - // Calculate position and width - const left = clip.start * zoom.pixelsPerSecond - const width = clip.duration * zoom.pixelsPerSecond - - // If dragging this clip, use new position - const finalLeft = drag.dragState.clipId === clip.id && drag.dragState.isDragging - ? drag.newStart * zoom.pixelsPerSecond - : left - - return ( - <div - className="clip" - style={{ - position: 'absolute', - left: `${finalLeft}px`, - width: `${width}px`, - height: '100%', - backgroundColor: clip.color || '#3b82f6', - cursor: drag.dragState.isDragging ? 'grabbing' : 'grab' - }} - onMouseDown={handleMouseDown} - > - <div className="clip-name">{clip.name}</div> - </div> - ) -}-
Registry Structure
-{
- "components": [
- {
- "name": "timeline",
- "type": "component",
- "files": ["components/timeline.tsx"],
- "dependencies": ["@timeline/react"],
- "registryDependencies": ["track", "playhead", "ruler"]
- },
- {
- "name": "track",
- "type": "component",
- "files": ["components/track.tsx"],
- "dependencies": ["@timeline/react"],
- "registryDependencies": ["clip"]
- },
- {
- "name": "clip",
- "type": "component",
- "files": ["components/clip.tsx"],
- "dependencies": ["@timeline/react", "@timeline/core"]
- }
- ]
-}
- - - - - - -
CLI Tool
-- The CLI is what makes your library feel like magic. One command and components appear in the user's codebase. -
- -CLI Commands
- -timeline init
-import * as fs from 'fs-extra' -import * as path from 'path' -import prompts from 'prompts' -import chalk from 'chalk' -import ora from 'ora' -import { execa } from 'execa' - -export async function init() { - console.log(chalk.bold('\nTimeline Library Setup\n')) - - // Detect framework - const framework = await detectFramework() - - // Ask user for configuration - const config = await prompts([ - { - type: 'select', - name: 'framework', - message: 'Which framework are you using?', - choices: [ - { title: 'React', value: 'react' }, - { title: 'Vue', value: 'vue' }, - { title: 'Svelte', value: 'svelte' } - ], - initial: framework === 'react' ? 0 : 1 - }, - { - type: 'text', - name: 'componentsPath', - message: 'Where should we install components?', - initial: 'src/components/timeline' - } - ]) - - // Install dependencies - const spinner = ora('Installing dependencies...').start() - - try { - await execa('npm', [ - 'install', - '@timeline/core', - `@timeline/${config.framework}` - ]) - - spinner.succeed('Dependencies installed') - } catch (error) { - spinner.fail('Failed to install dependencies') - throw error - } - - // Create config file - const configPath = path.join(process.cwd(), 'timeline.config.json') - await fs.writeJson(configPath, config, { spaces: 2 }) - - console.log(chalk.green('\nโ Timeline library initialized!\n')) - console.log('Next steps:') - console.log(chalk.cyan(' timeline add timeline') + ' - Install timeline component') -} - -async function detectFramework(): Promise<string> { - const packageJsonPath = path.join(process.cwd(), 'package.json') - - if (await fs.pathExists(packageJsonPath)) { - const packageJson = await fs.readJson(packageJsonPath) - const deps = { ...packageJson.dependencies, ...packageJson.devDependencies } - - if (deps.react) return 'react' - if (deps.vue) return 'vue' - if (deps.svelte) return 'svelte' - } - - return 'react' // default -}-
timeline add [component]
-import * as fs from 'fs-extra' -import * as path from 'path' -import chalk from 'chalk' -import ora from 'ora' - -interface Component { - name: string - files: string[] - dependencies: string[] - registryDependencies: string[] -} - -export async function add(componentName: string) { - // Load config - const config = await loadConfig() - - // Fetch registry - const spinner = ora('Fetching component registry...').start() - const registry = await fetchRegistry() - - // Find component - const component = registry.components.find( - (c: Component) => c.name === componentName - ) - - if (!component) { - spinner.fail(`Component "${componentName}" not found`) - return - } - - spinner.text = `Installing ${componentName}...` - - // Resolve all dependencies (recursively) - const allComponents = resolveDependencies(component, registry) - - // Copy component files - for (const comp of allComponents) { - for (const file of comp.files) { - const source = await fetchComponentFile(file) - const target = path.join( - process.cwd(), - config.componentsPath, - path.basename(file) - ) - - await fs.ensureDir(path.dirname(target)) - await fs.writeFile(target, source) - } - } - - spinner.succeed(`Installed ${componentName} and ${allComponents.length - 1} dependencies`) - - console.log('\n' + chalk.green('Files created:')) - allComponents.forEach(c => { - console.log(chalk.cyan(` - ${c.name}.tsx`)) - }) -} - -function resolveDependencies( - component: Component, - registry: any -): Component[] { - const result: Component[] = [component] - const visited = new Set<string>([component.name]) - - const resolve = (comp: Component) => { - for (const depName of comp.registryDependencies || []) { - if (visited.has(depName)) continue - - const dep = registry.components.find((c: Component) => c.name === depName) - if (dep) { - visited.add(depName) - result.push(dep) - resolve(dep) - } - } - } - - resolve(component) - return result -}-
Usage Example
-# Initialize timeline library in your project -npx timeline-cli init - -# Install timeline component (pulls in track, clip, playhead, ruler) -npx timeline-cli add timeline - -# Files are now in your src/components/timeline/ -# You own them - modify as needed!-
- - - - - -
Performance Optimization
-- A timeline with 1000+ clips must feel instant. Here's how to achieve that. -
- -Critical Optimizations
- -1. Virtualization
-Only render visible clips/tracks:
- -import { useVirtualizer } from '@tanstack/react-virtual' - -function Timeline() { - const parentRef = useRef<HTMLDivElement>(null) - const { timeline } = useTimelineContext() - - // Virtualize tracks - const rowVirtualizer = useVirtualizer({ - count: timeline.tracks.length, - getScrollElement: () => parentRef.current, - estimateSize: () => 64, // track height - overscan: 2 - }) - - return ( - <div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}> - <div style={{ height: rowVirtualizer.getTotalSize() }}> - {rowVirtualizer.getVirtualItems().map(virtualRow => ( - <Track - key={virtualRow.key} - track={timeline.tracks[virtualRow.index]} - style={{ - position: 'absolute', - top: 0, - left: 0, - width: '100%', - transform: `translateY(${virtualRow.start}px)` - }} - /> - ))} - </div> - </div> - ) -}-
2. Memoization
-const Clip = React.memo(({ clip, zoom }) => { - // Only recalculate position when clip or zoom changes - const position = useMemo( - () => ({ - left: clip.start * zoom, - width: clip.duration * zoom - }), - [clip.start, clip.duration, zoom] - ) - - return <div style={position}>{clip.name}</div> -})-
3. requestAnimationFrame for Dragging
-const handleMouseMove = useCallback((e: MouseEvent) => { - // Cancel previous frame - if (rafRef.current) { - cancelAnimationFrame(rafRef.current) - } - - // Schedule update for next frame - rafRef.current = requestAnimationFrame(() => { - setDragState(prev => ({ - ...prev, - currentX: e.clientX - })) - }) -}, [])-
Performance Checklist
-- - - - - -
Testing Strategy
-- Tests are not optional. They let you refactor with confidence and catch bugs before users do. -
- -Core Package Tests (Vitest)
-import { describe, it, expect } from 'vitest' -import { moveClip, splitClip } from '../src/mutations' -import { Timeline } from '../src/types' - -describe('moveClip', () => { - it('should move clip to new time', () => { - const timeline: Timeline = { - id: 'test', - tracks: [{ - id: 'track-1', - clips: [{ - id: 'clip-1', - start: 0, - duration: 5, - trackId: 'track-1' - }] - }] - } - - const result = moveClip(timeline, 'clip-1', 10) - - expect(result.tracks[0].clips[0].start).toBe(10) - }) - - it('should not mutate original timeline', () => { - const timeline = createTestTimeline() - const result = moveClip(timeline, 'clip-1', 10) - - expect(timeline).not.toBe(result) - expect(timeline.tracks[0].clips[0].start).toBe(0) - }) -})-
React Hooks Tests
-import { renderHook, act } from '@testing-library/react' -import { useTimeline } from '../src/hooks/useTimeline' - -describe('useTimeline', () => { - it('should move clip when action called', () => { - const timeline = createTestTimeline() - const { result } = renderHook(() => useTimeline(timeline)) - - act(() => { - result.current.actions.moveClip('clip-1', 10) - }) - - expect( - result.current.timeline.tracks[0].clips[0].start - ).toBe(10) - }) -})-
Test Coverage Goals
--
-
- Core package: 90%+ coverage (it's pure functions, easy to test) -
- React hooks: 80%+ coverage -
- Components: 60%+ coverage (focus on interaction logic) -
- - - - - -
Documentation Site
-- Great docs are what turn a library into a successful product. Invest heavily here. -
- -Documentation Structure
-docs/ -โโโ getting-started/ -โ โโโ installation.mdx -โ โโโ quickstart.mdx -โ โโโ concepts.mdx -โโโ api-reference/ -โ โโโ core/ -โ โ โโโ mutations.mdx -โ โ โโโ queries.mdx -โ โโโ react/ -โ โโโ useTimeline.mdx -โ โโโ usePlayback.mdx -โโโ components/ -โ โโโ timeline.mdx -โ โโโ track.mdx -โ โโโ clip.mdx -โโโ examples/ -โ โโโ basic-timeline.mdx -โ โโโ video-editor.mdx -โ โโโ audio-editor.mdx -โโโ guides/ - โโโ customization.mdx - โโโ performance.mdx - โโโ migration.mdx-
Interactive Examples
-Use code sandboxes or live preview:
- -# Basic Timeline Example - -Here's a minimal timeline with drag-and-drop: - -```tsx -import { Timeline } from '@/components/timeline' -import { TimelineProvider } from '@timeline/react' - -export function App() { - const timeline = { /* ... */ } - - return ( - <TimelineProvider initialTimeline={timeline}> - <Timeline /> - </TimelineProvider> - ) -} -``` - -<Preview> - <BasicTimelineDemo /> -</Preview>-
Documentation Essentials
-Getting Started
-5-minute quickstart that gets users to a working timeline
-API Reference
-Every function, hook, and prop documented with examples
-Component Gallery
-Visual showcase of all components with variations
-Guides
-Deep dives: customization, performance, advanced patterns
-- - - - - -
Common Pitfalls to Avoid
-- Learn from mistakes others have made. Here are the traps that will derail your project. -
- -๐ซ Pitfall #1: Mixing Layers
-// packages/core/src/mutations.ts -import { useState } from 'react' // โ NO!-
Why bad: Core package becomes React-only. Can't support Vue/Svelte.
-Fix: Keep core 100% framework-agnostic. No React, no DOM, no side effects.
-๐ซ Pitfall #2: Mutating State
-function moveClip(timeline, clipId, newStart) { - const clip = timeline.tracks[0].clips[0] - clip.start = newStart // โ Mutation! - return timeline -}-
Why bad: Breaks React's change detection. Undo/redo impossible. Hard to debug.
-Fix: Always return NEW objects. Use spread operators {...obj}.
-๐ซ Pitfall #3: Premature Optimization
-Building custom virtualization before you have 100 clips? Writing a WebWorker for calculations that take 2ms? Stop.
-Fix: Build it working first. Profile with Chrome DevTools. Optimize what's actually slow.
-๐ซ Pitfall #4: Over-Engineering
-Adding plugin systems, extension APIs, and event buses before you have basic drag-and-drop working?
-Fix: Ship core features first. Add extensibility when users ask for it.
-๐ซ Pitfall #5: Poor CLI UX
--
-
- No progress indicators during install -
- Cryptic error messages -
- Doesn't detect framework automatically -
- Overwrites user files without asking -
Fix: Add spinners (ora), helpful errors, confirmation prompts, auto-detection.
-๐ซ Pitfall #6: Skipping Tests
-No you won't. And then you'll break core functions while adding features.
-Fix: Write tests WHILE building. At minimum, test all core package functions.
-๐ซ Pitfall #7: Video Editor First
-Starting with video playback, encoding, effects before the timeline works?
-Fix: Build timeline library first. Make it excellent. Video features = Phase 2.
-โ Success Patterns
-Start Small
-Ship timeline library v1.0 with just core features. Iterate based on feedback.
-Respect Layers
-Core = pure. Adapters = thin. Components = user-owned. Never mix.
-Test Everything
-Core package must have 80%+ coverage. Sleep well at night.
-Document Well
-Docs = product. Invest in examples, guides, videos.
-- - - - - -
Development Roadmap
-- Here's the complete 14-week plan to ship v1.0. Follow this order โ resist the urge to jump ahead. -
- --
-
- Initialize monorepo: `npx create-turbo@latest timeline-library` -
- Set up packages: core, react, cli, docs -
- Configure TypeScript (strict mode), ESLint, Prettier -
- Write ALL type definitions in packages/core/src/types.ts -
- Set up Vitest for testing -
- Create GitHub repo with CI/CD (GitHub Actions) -
- Write README with vision and goals -
-
-
- Implement mutations.ts: moveClip, trimClip, splitClip, deleteClip -
- Implement mutations.ts: addTrack, deleteTrack, reorderTracks -
- Implement queries.ts: getActiveClipsAtTime, getNextEditPoint -
- Implement utils.ts: timeToPixels, pixelsToTime, snapToGrid -
- Write unit tests for EVERY function (aim for 90%+ coverage) -
- Add JSDoc comments to all public APIs -
- Build first version: `turbo build` -
-
-
- Create useTimeline() hook -
- Create useHistory() hook (undo/redo) -
- Create usePlayback() hook -
- Create useDrag() hook -
- Create useZoom() hook -
- Create TimelineContext provider -
- Build demo app in apps/playground to test hooks -
- Write tests for all hooks -
-
-
- Create Timeline component (root container) -
- Create Track component -
- Create Clip component (draggable, resizable) -
- Create Playhead component -
- Create TimeRuler component -
- Create TrackControls component -
- Add basic styling (Tailwind or plain CSS) -
- Create registry.json metadata -
- Test all components in playground app -
-
-
- Set up packages/cli with Commander.js -
- Implement `timeline init` command -
- Implement `timeline add [component]` command -
- Add framework detection (React/Vue/Svelte) -
- Handle dependency resolution -
- Add progress spinners and nice output -
- Test CLI in fresh React/Next.js project -
- Publish CLI to npm as `timeline-cli` -
-
-
- Set up docs site (Next.js + MDX or Astro) -
- Write Getting Started guide (installation, quickstart) -
- Write API reference for all core functions -
- Write API reference for all React hooks -
- Create component documentation with live examples -
- Write customization guide -
- Write performance guide -
- Add search functionality -
- Deploy docs to Vercel/Netlify -
- Record 3-5 minute demo video -
You're ready to ship when you have:
--
-
- โ Core package with 20+ functions, 90%+ test coverage -
- โ React adapter with 5+ hooks -
- โ 6+ UI components in registry -
- โ Working CLI that installs in 30 seconds -
- โ Documentation site with examples and API docs -
- โ Demo video showing key features -
- โ Published to npm: @timeline/core, @timeline/react, timeline-cli -
- -
Your Next Steps
-- You now have the complete blueprint. Here's exactly what to build, in what order, and how to avoid getting stuck. -
- -Development Phases
- --
-
- Initialize monorepo with Turborepo or pnpm workspaces -
- Create package structure: core, react, vue, cli, docs -
- Set up TypeScript configs with strict mode -
- Configure ESLint, Prettier, Vitest -
- Write all type definitions in packages/core/src/types.ts -
- Set up GitHub repo with CI/CD (GitHub Actions) -
-
-
- Implement clip manipulation: moveClip, trimClip, splitClip, deleteClip -
- Implement track management: addTrack, deleteTrack, reorderTracks -
- Implement query functions: getActiveClipsAtTime, getNextEditPoint -
- Implement time utilities: timeToPixels, pixelsToTime, snapToGrid -
- Write comprehensive unit tests for all functions (80%+ coverage) -
- Document all public APIs with JSDoc -
-
-
- Create useTimeline() hook for state management -
- Create useHistory() hook for undo/redo -
- Create useDrag() hook for drag interactions -
- Create usePlayback() hook for play/pause/scrub -
- Create useZoom() hook for zoom/pan controls -
- Build demo app to test all hooks -
-
-
- Create Timeline component (root container) -
- Create TimelineTrack component -
- Create TimelineClip component with drag/resize -
- Create Playhead component with scrubbing -
- Create TimeRuler component with time markers -
- Create TrackControls component (mute, solo, lock) -
- Add all components to registry/ for CLI -
-
-
- Set up packages/cli with Commander.js -
- Implement 'timeline init' command -
- Implement 'timeline add [component]' command -
- Handle framework detection (React, Vue, etc.) -
- Handle dependency installation automatically -
- Test CLI in fresh projects -
-
-
- Build docs site with Next.js or Astro -
- Write getting started guide -
- Document all core functions with examples -
- Create interactive component playground -
- Record video tutorials -
- Write migration guides for popular editors -
-
-
- Don't build everything at once โ Start with Phase 1-3, ship that, then iterate -
- Don't optimize prematurely โ Build it working first, then profile and optimize -
- Don't skip tests โ Core functions MUST have tests or you'll break things constantly -
- Don't couple layers โ Keep core, adapters, and UI completely separate -
- Don't build the video editor yet โ Timeline library first, video features later -
Success Criteria
-You're ready to ship v1.0 when you have:
-You have everything you need:
--
-
- Clear architecture (3-layer separation) -
- Data model (immutable timeline schema) -
- State management strategy (pure functions + history) -
- Technology choices (TypeScript, React, Turborepo) -
- Execution plan (14-week roadmap) -
Start with Phase 1. Get the types right. Build the core layer with tests. Everything else will flow naturally from there.
-You're not building a video editor โ you're building the foundation that enables a thousand video editors to be built. That's the real vision. Now go make it real.
-
-
-
-
-
-
-
-#### Videos
-
-Use the HTML video element for self-hosted video content:
-
-
-
-Embed YouTube videos using iframe elements:
-
-
-
-#### Tooltips
-
-Example of tooltip usage:
-
-
-
-
-## Code formatting
-
-We suggest using extensions on your IDE to recognize and format MDX. If you're a VSCode user, consider the [MDX VSCode extension](https://marketplace.visualstudio.com/items?itemName=unifiedjs.vscode-mdx) for syntax highlighting, and [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) for code formatting.
-
-## Troubleshooting
-
-
-
-## Image
-
-### Using Markdown
-
-The [markdown syntax](https://www.markdownguide.org/basic-syntax/#images) lets you add images using the following code
-
-```md
-
-```
-
-Note that the image file size must be less than 5MB. Otherwise, we recommend hosting on a service like [Cloudinary](https://cloudinary.com/) or [S3](https://aws.amazon.com/s3/). You can then use that URL and embed.
-
-### Using embeds
-
-To get more customizability with images, you can also use [embeds](/writing-content/embed) to add images
-
-```html
-
-```
-
-## Embeds and HTML elements
-
-
-
-