'use client'; import { useState, useCallback, useRef, useEffect } from 'react'; import { TimelineState, Section, SectionType, PatternKeyframe, ChordKeyframe, MuteKeyframe, AutomationPoint, LoopRegion, LayerName, SECTION_COLORS, } from '@/types/audio'; function generateId(): string { return Math.random().toString(36).substring(2, 9); } const defaultState: TimelineState = { durationBars: 16, playheadBar: 0, playheadBeat: 0, sections: [], drumKeyframes: [{ id: generateId(), bar: 0, patternIndex: 0 }], chordKeyframes: [{ id: generateId(), bar: 0, progressionIndex: 0 }], volumeAutomation: { drums: [], chords: [], ambient: [], }, muteKeyframes: { drums: [], chords: [], ambient: [], }, loopRegion: { start: 0, end: 16, enabled: false }, }; export function useTimeline() { const [state, setState] = useState(defaultState); const setDuration = useCallback((bars: number) => { setState((prev) => ({ ...prev, durationBars: bars, loopRegion: { ...prev.loopRegion, end: Math.min(prev.loopRegion.end, bars), }, })); }, []); const setPlayheadPosition = useCallback((bar: number, beat: number) => { setState((prev) => ({ ...prev, playheadBar: bar, playheadBeat: beat, })); }, []); const setLoopRegion = useCallback((start: number, end: number, enabled: boolean) => { setState((prev) => ({ ...prev, loopRegion: { start, end, enabled }, })); }, []); const addSection = useCallback( (type: SectionType, startBar: number, endBar: number) => { const newSection: Section = { id: generateId(), type, name: type.charAt(0).toUpperCase() + type.slice(1), startBar, endBar, color: SECTION_COLORS[type], }; setState((prev) => ({ ...prev, sections: [...prev.sections, newSection], })); }, [] ); const resizeSection = useCallback((id: string, startBar: number, endBar: number) => { setState((prev) => ({ ...prev, sections: prev.sections.map((s) => s.id === id ? { ...s, startBar, endBar } : s ), })); }, []); const moveSection = useCallback((id: string, startBar: number) => { setState((prev) => ({ ...prev, sections: prev.sections.map((s) => { if (s.id !== id) return s; const duration = s.endBar - s.startBar; return { ...s, startBar, endBar: startBar + duration }; }), })); }, []); const deleteSection = useCallback((id: string) => { setState((prev) => ({ ...prev, sections: prev.sections.filter((s) => s.id !== id), })); }, []); const addDrumKeyframe = useCallback((bar: number, patternIndex: number) => { const newKeyframe: PatternKeyframe = { id: generateId(), bar, patternIndex, }; setState((prev) => ({ ...prev, drumKeyframes: [...prev.drumKeyframes, newKeyframe], })); }, []); const updateDrumKeyframe = useCallback((id: string, patternIndex: number) => { setState((prev) => ({ ...prev, drumKeyframes: prev.drumKeyframes.map((kf) => kf.id === id ? { ...kf, patternIndex } : kf ), })); }, []); const deleteDrumKeyframe = useCallback((id: string) => { setState((prev) => ({ ...prev, drumKeyframes: prev.drumKeyframes.filter((kf) => kf.id !== id), })); }, []); const addChordKeyframe = useCallback((bar: number, progressionIndex: number) => { const newKeyframe: ChordKeyframe = { id: generateId(), bar, progressionIndex, }; setState((prev) => ({ ...prev, chordKeyframes: [...prev.chordKeyframes, newKeyframe], })); }, []); const updateChordKeyframe = useCallback((id: string, progressionIndex: number) => { setState((prev) => ({ ...prev, chordKeyframes: prev.chordKeyframes.map((kf) => kf.id === id ? { ...kf, progressionIndex } : kf ), })); }, []); const deleteChordKeyframe = useCallback((id: string) => { setState((prev) => ({ ...prev, chordKeyframes: prev.chordKeyframes.filter((kf) => kf.id !== id), })); }, []); const toggleMuteKeyframe = useCallback((layer: LayerName, bar: number) => { setState((prev) => { const keyframes = prev.muteKeyframes[layer]; const existing = keyframes.find((kf) => kf.bar === bar); if (existing) { return { ...prev, muteKeyframes: { ...prev.muteKeyframes, [layer]: keyframes.map((kf) => kf.id === existing.id ? { ...kf, muted: !kf.muted } : kf ), }, }; } const sorted = [...keyframes].sort((a, b) => a.bar - b.bar); const prevKeyframe = sorted.filter((kf) => kf.bar < bar).pop(); const wasMuted = prevKeyframe?.muted ?? false; const newKeyframe: MuteKeyframe = { id: generateId(), bar, muted: !wasMuted, }; return { ...prev, muteKeyframes: { ...prev.muteKeyframes, [layer]: [...keyframes, newKeyframe], }, }; }); }, []); const addAutomationPoint = useCallback( (layer: LayerName, bar: number, beat: number, value: number) => { const newPoint: AutomationPoint = { id: generateId(), bar, beat, value, }; setState((prev) => ({ ...prev, volumeAutomation: { ...prev.volumeAutomation, [layer]: [...prev.volumeAutomation[layer], newPoint], }, })); }, [] ); const updateAutomationPoint = useCallback( (layer: LayerName, id: string, bar: number, beat: number, value: number) => { setState((prev) => ({ ...prev, volumeAutomation: { ...prev.volumeAutomation, [layer]: prev.volumeAutomation[layer].map((p) => p.id === id ? { ...p, bar, beat, value } : p ), }, })); }, [] ); const deleteAutomationPoint = useCallback((layer: LayerName, id: string) => { setState((prev) => ({ ...prev, volumeAutomation: { ...prev.volumeAutomation, [layer]: prev.volumeAutomation[layer].filter((p) => p.id !== id), }, })); }, []); const resetTimeline = useCallback(() => { setState(defaultState); }, []); return { state, setDuration, setPlayheadPosition, setLoopRegion, addSection, resizeSection, moveSection, deleteSection, addDrumKeyframe, updateDrumKeyframe, deleteDrumKeyframe, addChordKeyframe, updateChordKeyframe, deleteChordKeyframe, toggleMuteKeyframe, addAutomationPoint, updateAutomationPoint, deleteAutomationPoint, resetTimeline, }; }