Live demo
Code example
import { TimelineEngine, TimelineProvider, Timeline, useTimeline, CanvasRenderer, fromSeconds,} from '@techsquidtv/canvas-timeline';import { useMemo } from 'react';import { demoMarkers, demoTracks } from './timeline-demo-data';import '@techsquidtv/canvas-timeline/styles.css';
function CustomInPointGrabber({ dragging }: { dragging: boolean }) { return ( <div className="relative h-full w-full"> <div className={`absolute bottom-0 left-1/2 top-0 w-[2px] -translate-x-1/2 bg-gradient-to-b from-emerald-500 to-transparent transition-all duration-150 ${ dragging ? 'opacity-100 shadow-[0_0_8px_#10b981]' : 'opacity-85' }`} style={{ pointerEvents: 'auto' }} /> </div> );}
function CustomOutPointGrabber({ dragging }: { dragging: boolean }) { return ( <div className="relative h-full w-full"> <div className={`absolute bottom-0 left-1/2 top-0 w-[2px] -translate-x-1/2 bg-gradient-to-b from-rose-500 to-transparent transition-all duration-150 ${ dragging ? 'opacity-100 shadow-[0_0_8px_#f43f5e]' : 'opacity-85' }`} style={{ pointerEvents: 'auto' }} /> </div> );}
function TimelineLayers() { const { state } = useTimeline();
return ( <> <Timeline.PlayheadArea />
{/* Composed custom playhead grabber using the render prop interface */} <Timeline.PlayheadGrabber> {({ dragging }) => { return ( <div className="relative h-full w-full"> {/* Custom Handle Diamond */} <div className={`absolute left-1/2 -ml-1.5 top-[2px] w-3 h-3 rotate-45 border border-violet-500 bg-gradient-to-br from-violet-600 to-indigo-700 shadow-sm transition-all duration-150 ${ dragging ? 'scale-110 opacity-95 shadow-violet-500/50' : 'opacity-80 hover:opacity-95' }`} style={{ pointerEvents: 'auto' }} />
{/* Custom 2px Glowing Gradient Line (Starts at the bottom tip of the diamond handle) */} <div className={`absolute left-1/2 -ml-[1px] top-[16px] bottom-0 w-[2px] bg-gradient-to-b from-violet-500 via-indigo-500 to-transparent transition-all duration-150 ${ dragging ? 'opacity-100 shadow-[0_0_8px_#8b5cf6]' : 'opacity-85' }`} style={{ pointerEvents: 'auto' }} /> </div> ); }} </Timeline.PlayheadGrabber>
<Timeline.TrackList className="timeline-track-list-overlay"> {state.tracks.map((track) => ( <Timeline.Track key={track.id} trackId={track.id} /> ))} </Timeline.TrackList> <Timeline.ClipInteractionLayer /> <Timeline.RangeSelector inPointChildren={CustomInPointGrabber} outPointChildren={CustomOutPointGrabber} /> </> );}
export function CustomPlayheadTimeline() { const engine = useMemo(() => { const timeline = new TimelineEngine({ duration: fromSeconds(15), playheadTime: fromSeconds(2), zoomScale: 74, tracks: demoTracks, markers: demoMarkers, }); timeline.setInPoint(fromSeconds(3)); timeline.setOutPoint(fromSeconds(11)); return timeline; }, []);
return ( <TimelineProvider engine={engine}> <div className="timeline-shell"> <div className="timeline-stage"> <Timeline.Root className="timeline-fill"> <CanvasRenderer /> <TimelineLayers /> </Timeline.Root> </div> <div className="timeline-scrollbar-row"> <Timeline.ViewportScrollbar> <Timeline.ViewportScrollbarThumb> <Timeline.ViewportScrollbarHandle side="start" /> <Timeline.ViewportScrollbarHandle side="end" /> </Timeline.ViewportScrollbarThumb> </Timeline.ViewportScrollbar> </div> </div> </TimelineProvider> );}const demoClipColors = [ 'oklch(0.62 0.16 250)', 'oklch(0.68 0.14 145)', 'oklch(0.72 0.16 70)', 'oklch(0.65 0.17 25)', 'oklch(0.58 0.18 305)', 'oklch(0.64 0.12 195)',] as const;
export function getDemoClipColor(index: number) { return demoClipColors[index % demoClipColors.length];}import { type Marker, type Track, fromSeconds } from '@techsquidtv/canvas-timeline';import { getDemoClipColor } from '../demo-clip-colors';
export const demoTracks: Track<'visual' | 'audio'>[] = [ { id: 'video-a', kind: 'visual', name: 'Video A', locked: false, muted: false, visible: true, selected: false, height: 48, clips: [ { id: 'intro', sourceId: 'vid-intro', timelineStart: fromSeconds(1), timelineEnd: fromSeconds(5.5), sourceStart: fromSeconds(0), selected: false, color: getDemoClipColor(0), label: 'Intro sequence', }, { id: 'main', sourceId: 'vid-main', timelineStart: fromSeconds(6.5), timelineEnd: fromSeconds(12.5), sourceStart: fromSeconds(0), selected: false, color: getDemoClipColor(1), label: 'Main feature clip', }, ], }, { id: 'overlay-b', kind: 'visual', name: 'Overlay B', locked: false, muted: false, visible: true, selected: false, height: 48, clips: [ { id: 'b-roll', sourceId: 'vid-overlay', timelineStart: fromSeconds(3), timelineEnd: fromSeconds(8.5), sourceStart: fromSeconds(0), selected: false, color: getDemoClipColor(2), label: 'B-roll overlay', }, ], }, { id: 'audio-a', kind: 'audio', name: 'Ambient Soundtrack', locked: false, muted: false, visible: true, selected: false, height: 48, clips: [ { id: 'score', sourceId: 'aud-score', timelineStart: fromSeconds(0), timelineEnd: fromSeconds(15), sourceStart: fromSeconds(0), selected: false, color: getDemoClipColor(3), label: 'Background score', }, ], },];
export const demoMarkers: Marker[] = [ { id: 'm1', time: fromSeconds(1), label: 'M1' }, { id: 'm2', time: fromSeconds(6.5), label: 'M2' },];