mini-timeline
v0.1.9
Published
A lightweight, dependency-free React timeline editor with tracks, sub-tracks, playback, trimming, and JSON persistence.
Maintainers
Readme
DECADE.TW-mini-timeline
A lightweight React timeline editor with parent tracks, collapsible sub-tracks, smooth playback, clip editing, and optional JSON persistence. It has no runtime dependency beyond React and does not use a timeline UI library.
Screenshot

Features
- Controlled and uncontrolled state
- Parent tracks and one-level sub-tracks
- Add, rename, expand, collapse, and delete lanes
- Create, move, cross-lane move, trim, duplicate, and delete clips
- Custom clip types, labels, colors, and default data
- Editable clip name, start, end, and length
- Editable total duration constrained by content
- Playback rate, seeking, auto-scroll, and smooth playhead
- Command/Meta-wheel zoom, Ctrl-wheel horizontal scroll, contained normal wheel, and right-drag pan
- Generic enter, update, leave, time, and active-clip callbacks
- Optional localStorage persistence
- Versioned JSON import/export with legacy-array import support
- ESM, CommonJS, CSS, source maps, and TypeScript declarations
Install
npm install mini-timelineimport { MiniTimeline } from 'mini-timeline';
import 'mini-timeline/style.css';The package name is provisional. Verify npm name availability and update package.json before publishing if necessary.
Minimal uncontrolled example
const initialTracks = [
{
id: 'track-1',
name: 'Main',
collapsed: false,
actions: [
{
id: 'clip-1',
start: 0,
end: 2,
type: 'cue',
data: { name: 'Opening' }
}
],
subTracks: []
}
];
export function Editor() {
return (
<MiniTimeline
defaultValue={initialTracks}
defaultDuration={60}
storageKey="my-project-timeline"
clipTypes={[
{ id: 'cue', label: 'Cue', color: '#16747d', defaultData: { name: 'Cue' } },
{ id: 'audio', label: 'Audio', color: '#9a681e', defaultData: { name: 'Audio' } },
{ id: 'animation', label: 'Animation', color: '#7349a4', defaultData: { name: 'Animation' } }
]}
/>
);
}Controlled example
import { useState } from 'react';
import { MiniTimeline } from 'mini-timeline';
import 'mini-timeline/style.css';
export function ControlledEditor() {
const [tracks, setTracks] = useState([]);
const [duration, setDuration] = useState(120);
return (
<MiniTimeline
value={tracks}
onChange={(nextTracks, detail) => {
console.log(detail.type);
setTracks(nextTracks);
}}
duration={duration}
onDurationChange={setDuration}
clipTypes={[{ id: 'event', label: 'Event', color: '#2563eb' }]}
/>
);
}Lifecycle integration
The package does not play audio or send network messages itself. Consumers connect effects through callbacks:
<MiniTimeline
value={tracks}
onChange={setTracks}
clipTypes={clipTypes}
onClipEnter={(clip, context) => {
if (clip.type === 'audio') audioEngine.play(clip.data.src, context.time - clip.start);
if (clip.type === 'cue') udp.send(clip.data.startMessage);
}}
onClipUpdate={(clip, context) => {
animationEngine.seek(clip.id, context.time - clip.start);
}}
onClipLeave={(clip) => {
if (clip.type === 'audio') audioEngine.stop(clip.id);
if (clip.type === 'cue') udp.send(clip.data.stopMessage);
}}
onActiveClipChange={(clip) => console.log('Active clip:', clip)}
/>Lifecycle context contains time, previousTime, playing, rate, track, and subTrack.
Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| value | TimelineTrack[] | — | Controlled track data |
| defaultValue | TimelineTrack[] | [] | Initial uncontrolled data |
| onChange | (tracks, detail) => void | — | Called for every data mutation |
| clipTypes | ClipTypeDefinition[] | Default Clip | Available clip types |
| duration | number | — | Controlled total duration |
| defaultDuration | number | 60 | Initial/minimum uncontrolled duration |
| onDurationChange | (duration) => void | — | Called after duration edits/import |
| storageKey | string | — | Enables localStorage mirroring |
| snapInterval | number | 0.1 | Movement/property snap in seconds |
| minDuration | number | 0.1 | Minimum clip duration |
| playbackRate | number | — | Controlled playback rate |
| defaultPlaybackRate | number | 1 | Initial uncontrolled rate |
| onPlaybackRateChange | (rate) => void | — | Playback rate callback |
| onTimeChange | (time) => void | — | Throttled playback and immediate seek callback |
| onClipEnter | (clip, context) => void | — | Called when playhead enters a clip |
| onClipUpdate | (clip, context) => void | — | Called during active playback |
| onClipLeave | (clip, context) => void | — | Called on leave/pause/seek/unmount |
| onActiveClipChange | (clipOrNull) => void | — | First active clip callback |
| className | string | '' | Additional root class |
| ariaLabel | string | Timeline editor | Accessible editor label |
Data schema
interface TimelineClip {
id: string;
start: number;
end: number;
type: string;
data: { name?: string; [key: string]: unknown };
}
interface TimelineSubTrack {
id: string;
name: string;
actions: TimelineClip[];
}
interface TimelineTrack {
id: string;
name: string;
collapsed?: boolean;
actions: TimelineClip[];
subTracks?: TimelineSubTrack[];
}Normalization accepts legacy clips containing effectId; output uses type.
Persistence and JSON files
When storageKey is set, every change is mirrored as:
{
"version": 1,
"duration": 120,
"tracks": []
}Export downloads this schema. Import accepts both the versioned document and legacy root track arrays. Malformed imports leave current state unchanged and display an error.
Total duration cannot be less than the latest clip end.
Mouse and keyboard controls
| Input | Action | | --- | --- | | Space | Play/pause outside editable fields | | Delete or Backspace | Delete selected clip outside editable fields | | Double-click empty lane | Create clip | | Drag clip | Move in time or across visible lanes | | Drag clip edge | Trim | | Right-click clip | Duplicate/Delete menu | | Click/drag ruler | Seek | | Command/Meta + wheel | Zoom around pointer | | Ctrl + wheel | Horizontal scroll | | Normal wheel | Scroll inside timeline | | Right-drag empty timeline | Pan |
Styling
Override CSS custom properties on .mini-timeline:
.my-editor {
--mt-bg: #090b10;
--mt-panel: #131722;
--mt-border: #344258;
--mt-text: #f5f7fb;
--mt-muted: #93a3b6;
--mt-focus: #60a5fa;
--mt-playhead: #fb7185;
--mt-header-width: 240px;
}Clip colors come from clipTypes.
Browser support and accessibility
The package targets modern browsers supporting Pointer Events, requestAnimationFrame, CSS custom properties, and color-mix(). It provides accessible names, visible focus states, live status regions, keyboard guards for editable fields, and text labels in addition to clip colors.
Build and package check
npm install
npm run lint
npm run build
npm run pack-checkPublishing is intentionally not automatic. After selecting an available package name and authenticating with npm:
npm publish