'use client'; import { useCallback, useRef, useState } from 'react'; import { Drum, Music, Cloud, Volume2, VolumeX } from 'lucide-react'; import { Toggle } from '@/components/ui/toggle'; import { TimelineRuler } from './TimelineRuler'; import { Playhead } from './Playhead'; import { TimeDisplay } from './TimeDisplay'; import { DurationSelector } from './DurationSelector'; import { LoopBracket } from './LoopBracket'; import { SectionTrack } from './SectionTrack'; import { KeyframeTrack } from './KeyframeTrack'; import { PatternPicker } from './PatternPicker'; import { ProgressionPicker } from './ProgressionPicker'; import { MuteTrack } from './MuteTrack'; import { AutomationLane } from './AutomationLane'; import { cn } from '@/lib/utils'; import { TimelineState, LayerName, PatternKeyframe, ChordKeyframe, SectionType, } from '@/types/audio'; interface TimelineProps { state: TimelineState; bpm: number; isPlaying: boolean; pixelsPerBar?: number; onDurationChange: (bars: number) => void; onSeek: (bar: number, beat: number) => void; onLoopRegionChange: (start: number, end: number, enabled: boolean) => void; onSectionAdd: (type: SectionType, startBar: number, endBar: number) => void; onSectionResize: (id: string, startBar: number, endBar: number) => void; onSectionMove: (id: string, startBar: number) => void; onSectionDelete: (id: string) => void; onDrumKeyframeAdd: (bar: number, patternIndex: number) => void; onDrumKeyframeUpdate: (id: string, patternIndex: number) => void; onDrumKeyframeDelete: (id: string) => void; onChordKeyframeAdd: (bar: number, progressionIndex: number) => void; onChordKeyframeUpdate: (id: string, progressionIndex: number) => void; onChordKeyframeDelete: (id: string) => void; onMuteKeyframeToggle: (layer: LayerName, bar: number) => void; onAutomationPointAdd: ( layer: LayerName, bar: number, beat: number, value: number ) => void; onAutomationPointUpdate: ( layer: LayerName, id: string, bar: number, beat: number, value: number ) => void; onAutomationPointDelete: (layer: LayerName, id: string) => void; className?: string; } const PIXELS_PER_BAR = 48; export function Timeline({ state, bpm, isPlaying, pixelsPerBar = PIXELS_PER_BAR, onDurationChange, onSeek, onLoopRegionChange, onSectionAdd, onSectionResize, onSectionMove, onSectionDelete, onDrumKeyframeAdd, onDrumKeyframeUpdate, onDrumKeyframeDelete, onChordKeyframeAdd, onChordKeyframeUpdate, onChordKeyframeDelete, onMuteKeyframeToggle, onAutomationPointAdd, onAutomationPointUpdate, onAutomationPointDelete, className, }: TimelineProps) { const containerRef = useRef(null); const [selectedSectionId, setSelectedSectionId] = useState(null); const [selectedDrumKeyframeId, setSelectedDrumKeyframeId] = useState(null); const [selectedChordKeyframeId, setSelectedChordKeyframeId] = useState(null); const [expandedLayers, setExpandedLayers] = useState>({ drums: false, chords: false, ambient: false, }); const toggleLayerExpand = (layer: LayerName) => { setExpandedLayers((prev) => ({ ...prev, [layer]: !prev[layer] })); }; const handleRulerClick = useCallback( (e: React.MouseEvent) => { const rect = e.currentTarget.getBoundingClientRect(); const x = e.clientX - rect.left - 80; const totalBeats = x / (pixelsPerBar / 4); const bar = Math.floor(totalBeats / 4); const beat = Math.floor(totalBeats % 4); if (bar >= 0 && bar < state.durationBars) { onSeek(bar, beat); } }, [pixelsPerBar, state.durationBars, onSeek] ); const totalHeight = 24 + 20 + 32 + 32 + 32 + (expandedLayers.drums ? 48 : 0) + (expandedLayers.chords ? 48 : 0) + (expandedLayers.ambient ? 48 : 0) + 24 + 24 + 24; return (
onLoopRegionChange(r.start, r.end, r.enabled)} />
label="Drums" icon={} keyframes={state.drumKeyframes} durationBars={state.durationBars} pixelsPerBar={pixelsPerBar} selectedId={selectedDrumKeyframeId} onSelect={setSelectedDrumKeyframeId} onAdd={(bar) => onDrumKeyframeAdd(bar, 0)} onDelete={onDrumKeyframeDelete} markerColor="bg-lofi-orange" renderPicker={({ keyframe, onClose }) => ( onDrumKeyframeUpdate(keyframe.id, idx)} onRandom={() => onDrumKeyframeUpdate(keyframe.id, Math.floor(Math.random() * 5))} onClose={onClose} /> )} />
toggleLayerExpand('drums')} size="sm" className="h-4 px-1 text-[9px]" > {expandedLayers.drums ? 'Hide' : 'Auto'}
{expandedLayers.drums && ( } points={state.volumeAutomation.drums} durationBars={state.durationBars} pixelsPerBar={pixelsPerBar} onAddPoint={(bar, beat, value) => onAutomationPointAdd('drums', bar, beat, value)} onUpdatePoint={(id, bar, beat, value) => onAutomationPointUpdate('drums', id, bar, beat, value) } onDeletePoint={(id) => onAutomationPointDelete('drums', id)} /> )} label="Chords" icon={} keyframes={state.chordKeyframes} durationBars={state.durationBars} pixelsPerBar={pixelsPerBar} selectedId={selectedChordKeyframeId} onSelect={setSelectedChordKeyframeId} onAdd={(bar) => onChordKeyframeAdd(bar, 0)} onDelete={onChordKeyframeDelete} markerColor="bg-lofi-pink" renderPicker={({ keyframe, onClose }) => ( onChordKeyframeUpdate(keyframe.id, idx)} onRandom={() => onChordKeyframeUpdate(keyframe.id, Math.floor(Math.random() * 5))} onClose={onClose} /> )} />
toggleLayerExpand('chords')} size="sm" className="h-4 px-1 text-[9px]" > {expandedLayers.chords ? 'Hide' : 'Auto'}
{expandedLayers.chords && ( } points={state.volumeAutomation.chords} durationBars={state.durationBars} pixelsPerBar={pixelsPerBar} onAddPoint={(bar, beat, value) => onAutomationPointAdd('chords', bar, beat, value)} onUpdatePoint={(id, bar, beat, value) => onAutomationPointUpdate('chords', id, bar, beat, value) } onDeletePoint={(id) => onAutomationPointDelete('chords', id)} /> )} } keyframes={state.muteKeyframes.ambient} durationBars={state.durationBars} pixelsPerBar={pixelsPerBar} onToggle={(bar) => onMuteKeyframeToggle('ambient', bar)} />
toggleLayerExpand('ambient')} size="sm" className="h-4 px-1 text-[9px]" > {expandedLayers.ambient ? 'Hide' : 'Auto'}
{expandedLayers.ambient && ( } points={state.volumeAutomation.ambient} durationBars={state.durationBars} pixelsPerBar={pixelsPerBar} onAddPoint={(bar, beat, value) => onAutomationPointAdd('ambient', bar, beat, value)} onUpdatePoint={(id, bar, beat, value) => onAutomationPointUpdate('ambient', id, bar, beat, value) } onDeletePoint={(id) => onAutomationPointDelete('ambient', id)} /> )}
); }