'use client'; import * as React from 'react'; import { Plus, Upload, ChevronDown, ChevronRight, ChevronUp, X, Eye, EyeOff } from 'lucide-react'; import { Button } from '@/components/ui/Button'; import { cn } from '@/lib/utils/cn'; import { Track } from './Track'; import { TrackExtensions } from './TrackExtensions'; import { ImportTrackDialog } from './ImportTrackDialog'; import type { Track as TrackType } from '@/types/track'; import { DEFAULT_TRACK_HEIGHT, COLLAPSED_TRACK_HEIGHT, MIN_TRACK_HEIGHT } from '@/types/track'; import { createEffect, type EffectType, EFFECT_NAMES } from '@/lib/audio/effects/chain'; import { AutomationLane } from '@/components/automation/AutomationLane'; import { AutomationHeader } from '@/components/automation/AutomationHeader'; import type { AutomationPoint as AutomationPointType } from '@/types/automation'; import { createAutomationPoint } from '@/lib/audio/automation/utils'; import { EffectDevice } from '@/components/effects/EffectDevice'; import { EffectBrowser } from '@/components/effects/EffectBrowser'; export interface TrackListProps { tracks: TrackType[]; zoom: number; currentTime: number; duration: number; selectedTrackId?: string | null; onSelectTrack?: (trackId: string | null) => void; onAddTrack: () => void; onImportTrack?: (buffer: AudioBuffer, name: string) => void; onRemoveTrack: (trackId: string) => void; onUpdateTrack: (trackId: string, updates: Partial) => void; onSeek?: (time: number) => void; onSelectionChange?: (trackId: string, selection: { start: number; end: number } | null) => void; onToggleRecordEnable?: (trackId: string) => void; recordingTrackId?: string | null; recordingLevel?: number; trackLevels?: Record; onParameterTouched?: (trackId: string, laneId: string, touched: boolean) => void; isPlaying?: boolean; timeScaleScrollRef?: React.MutableRefObject; onTimeScaleScroll?: () => void; timeScaleScrollHandlerRef?: React.MutableRefObject<(() => void) | null>; } export function TrackList({ tracks, zoom, currentTime, duration, selectedTrackId, onSelectTrack, onAddTrack, onImportTrack, onRemoveTrack, onUpdateTrack, onSeek, onSelectionChange, onToggleRecordEnable, recordingTrackId, recordingLevel = 0, trackLevels = {}, onParameterTouched, isPlaying = false, timeScaleScrollRef: externalTimeScaleScrollRef, onTimeScaleScroll, timeScaleScrollHandlerRef, }: TrackListProps) { const [importDialogOpen, setImportDialogOpen] = React.useState(false); const [effectBrowserTrackId, setEffectBrowserTrackId] = React.useState(null); const waveformScrollRef = React.useRef(null); const controlsScrollRef = React.useRef(null); // Refs for horizontal scroll synchronization (per track) const waveformHScrollRefs = React.useRef>(new Map()); const automationHScrollRefs = React.useRef>(new Map()); const localTimeScaleScrollRef = React.useRef(null); const timeScaleScrollRef = externalTimeScaleScrollRef || localTimeScaleScrollRef; const [syncingScroll, setSyncingScroll] = React.useState(false); // Synchronize vertical scroll between controls and waveforms const handleWaveformScroll = React.useCallback(() => { if (waveformScrollRef.current && controlsScrollRef.current) { controlsScrollRef.current.scrollTop = waveformScrollRef.current.scrollTop; } }, []); // Synchronize horizontal scroll across all tracks (waveforms and automation lanes) const handleWaveformHScroll = React.useCallback((trackId: string) => { if (syncingScroll) return; setSyncingScroll(true); const sourceEl = waveformHScrollRefs.current.get(trackId); if (!sourceEl) { setSyncingScroll(false); return; } const scrollLeft = sourceEl.scrollLeft; // Sync all waveforms waveformHScrollRefs.current.forEach((el, id) => { if (id !== trackId) { el.scrollLeft = scrollLeft; } }); // Sync all automation lanes automationHScrollRefs.current.forEach((el) => { el.scrollLeft = scrollLeft; }); // Sync time scale if (timeScaleScrollRef.current) { timeScaleScrollRef.current.scrollLeft = scrollLeft; } setSyncingScroll(false); }, [syncingScroll]); const handleAutomationHScroll = React.useCallback((trackId: string) => { if (syncingScroll) return; setSyncingScroll(true); const sourceEl = automationHScrollRefs.current.get(trackId); if (!sourceEl) { setSyncingScroll(false); return; } const scrollLeft = sourceEl.scrollLeft; // Sync all waveforms waveformHScrollRefs.current.forEach((el) => { el.scrollLeft = scrollLeft; }); // Sync all automation lanes automationHScrollRefs.current.forEach((el, id) => { if (id !== trackId) { el.scrollLeft = scrollLeft; } }); // Sync time scale if (timeScaleScrollRef.current) { timeScaleScrollRef.current.scrollLeft = scrollLeft; } setSyncingScroll(false); }, [syncingScroll]); const handleTimeScaleScrollInternal = React.useCallback(() => { if (syncingScroll) return; setSyncingScroll(true); if (!timeScaleScrollRef.current) { setSyncingScroll(false); return; } const scrollLeft = timeScaleScrollRef.current.scrollLeft; // Sync all waveforms waveformHScrollRefs.current.forEach((el) => { el.scrollLeft = scrollLeft; }); // Sync all automation lanes automationHScrollRefs.current.forEach((el) => { el.scrollLeft = scrollLeft; }); setSyncingScroll(false); }, [syncingScroll]); // Expose the scroll handler via ref so AudioEditor can call it React.useEffect(() => { if (timeScaleScrollHandlerRef) { timeScaleScrollHandlerRef.current = handleTimeScaleScrollInternal; } }, [handleTimeScaleScrollInternal, timeScaleScrollHandlerRef]); const handleImportTrack = (buffer: AudioBuffer, name: string) => { if (onImportTrack) { onImportTrack(buffer, name); } }; if (tracks.length === 0) { return ( <>

No tracks yet. Add a track to get started.

{onImportTrack && ( )}
{onImportTrack && ( setImportDialogOpen(false)} onImportTrack={handleImportTrack} /> )} ); } return (
{/* Mobile Layout - Single Column (Stacked: Controls → Waveform per track) */}
{tracks.map((track) => (
{/* Track Controls - Top */} onSelectTrack(track.id) : undefined} onToggleMute={() => onUpdateTrack(track.id, { mute: !track.mute })} onToggleSolo={() => onUpdateTrack(track.id, { solo: !track.solo })} onToggleCollapse={() => onUpdateTrack(track.id, { collapsed: !track.collapsed })} onVolumeChange={(volume) => onUpdateTrack(track.id, { volume })} onPanChange={(pan) => onUpdateTrack(track.id, { pan })} onRemove={() => onRemoveTrack(track.id)} onNameChange={(name) => onUpdateTrack(track.id, { name })} onUpdateTrack={onUpdateTrack} onSeek={onSeek} onLoadAudio={(buffer) => onUpdateTrack(track.id, { audioBuffer: buffer })} onToggleEffect={(effectId) => { const updatedChain = { ...track.effectChain, effects: track.effectChain.effects.map((e) => e.id === effectId ? { ...e, enabled: !e.enabled } : e ), }; onUpdateTrack(track.id, { effectChain: updatedChain }); }} onRemoveEffect={(effectId) => { const updatedChain = { ...track.effectChain, effects: track.effectChain.effects.filter((e) => e.id !== effectId), }; onUpdateTrack(track.id, { effectChain: updatedChain }); }} onUpdateEffect={(effectId, parameters) => { const updatedChain = { ...track.effectChain, effects: track.effectChain.effects.map((e) => e.id === effectId ? { ...e, parameters } : e ), }; onUpdateTrack(track.id, { effectChain: updatedChain }); }} onAddEffect={(effectType) => { const newEffect = createEffect(effectType, EFFECT_NAMES[effectType]); const updatedChain = { ...track.effectChain, effects: [...track.effectChain.effects, newEffect], }; onUpdateTrack(track.id, { effectChain: updatedChain }); }} onSelectionChange={ onSelectionChange ? (selection) => onSelectionChange(track.id, selection) : undefined } onToggleRecordEnable={ onToggleRecordEnable ? () => onToggleRecordEnable(track.id) : undefined } isRecording={recordingTrackId === track.id} recordingLevel={recordingTrackId === track.id ? recordingLevel : 0} playbackLevel={trackLevels[track.id] || 0} onParameterTouched={onParameterTouched} isPlaying={isPlaying} renderControlsOnly={true} /> {/* Track Waveform with Automation and Effects - Bottom */} {!track.collapsed && (
{/* Waveform */}
onSelectTrack(track.id) : undefined} onToggleMute={() => onUpdateTrack(track.id, { mute: !track.mute })} onToggleSolo={() => onUpdateTrack(track.id, { solo: !track.solo })} onToggleCollapse={() => onUpdateTrack(track.id, { collapsed: !track.collapsed })} onVolumeChange={(volume) => onUpdateTrack(track.id, { volume })} onPanChange={(pan) => onUpdateTrack(track.id, { pan })} onRemove={() => onRemoveTrack(track.id)} onNameChange={(name) => onUpdateTrack(track.id, { name })} onUpdateTrack={onUpdateTrack} onSeek={onSeek} onLoadAudio={(buffer) => onUpdateTrack(track.id, { audioBuffer: buffer })} onToggleEffect={(effectId) => { const updatedChain = { ...track.effectChain, effects: track.effectChain.effects.map((e) => e.id === effectId ? { ...e, enabled: !e.enabled } : e ), }; onUpdateTrack(track.id, { effectChain: updatedChain }); }} onRemoveEffect={(effectId) => { const updatedChain = { ...track.effectChain, effects: track.effectChain.effects.filter((e) => e.id !== effectId), }; onUpdateTrack(track.id, { effectChain: updatedChain }); }} onUpdateEffect={(effectId, parameters) => { const updatedChain = { ...track.effectChain, effects: track.effectChain.effects.map((e) => e.id === effectId ? { ...e, parameters } : e ), }; onUpdateTrack(track.id, { effectChain: updatedChain }); }} onAddEffect={(effectType) => { const newEffect = createEffect(effectType, EFFECT_NAMES[effectType]); const updatedChain = { ...track.effectChain, effects: [...track.effectChain.effects, newEffect], }; onUpdateTrack(track.id, { effectChain: updatedChain }); }} onSelectionChange={ onSelectionChange ? (selection) => onSelectionChange(track.id, selection) : undefined } onToggleRecordEnable={ onToggleRecordEnable ? () => onToggleRecordEnable(track.id) : undefined } isRecording={recordingTrackId === track.id} recordingLevel={recordingTrackId === track.id ? recordingLevel : 0} playbackLevel={trackLevels[track.id] || 0} onParameterTouched={onParameterTouched} isPlaying={isPlaying} renderWaveformOnly={true} />
{/* Automation Bar */} {(() => { const selectedParam = track.automation.selectedParameterId || 'volume'; const currentLane = track.automation.lanes.find( l => l.parameterId === selectedParam ); // Build available parameters list const availableParameters: Array<{ id: string; name: string }> = [ { id: 'volume', name: 'Volume' }, { id: 'pan', name: 'Pan' }, ]; // Add effect parameters track.effectChain.effects.forEach((effect) => { if (effect.parameters) { Object.keys(effect.parameters).forEach((paramKey) => { const parameterId = `effect.${effect.id}.${paramKey}`; const paramName = `${effect.name} - ${paramKey.charAt(0).toUpperCase() + paramKey.slice(1)}`; availableParameters.push({ id: parameterId, name: paramName }); }); } }); // Get parameters that have automation lanes with points const automatedParams = track.automation.lanes .filter(lane => lane.points.length > 0) .map(lane => { const param = availableParameters.find(p => p.id === lane.parameterId); return param ? param.name : lane.parameterName; }); const modes = ['read', 'write', 'touch', 'latch'] as const; const MODE_LABELS = { read: 'R', write: 'W', touch: 'T', latch: 'L' }; const MODE_COLORS = { read: 'text-muted-foreground', write: 'text-red-500', touch: 'text-yellow-500', latch: 'text-orange-500', }; const currentModeIndex = modes.indexOf(currentLane?.mode || 'read'); return (
{/* Automation Header */}
Automation {/* Color indicator */} {currentLane?.color && (
)} {/* Parameter labels - always visible */}
{automatedParams.map((paramName, index) => ( {paramName} ))}
{/* Controls - only visible when expanded */} {track.automationExpanded && ( <> {/* Parameter selector */} {availableParameters && availableParameters.length > 1 && ( )} {/* Automation mode button */} {/* Height controls */}
)} {/* Show/hide toggle */}
{/* Automation Lane Content - Shown when expanded */} {track.automationExpanded && (
= 1 ? `${duration * zoom * 5}px` : '100%', }} > {track.automation.lanes .filter((lane) => lane.parameterId === (track.automation.selectedParameterId || 'volume') && lane.visible) .map((lane) => ( { const newPoint = createAutomationPoint({ time, value, curve: 'linear' }); const updatedLanes = track.automation.lanes.map((l) => l.id === lane.id ? { ...l, points: [...l.points, newPoint].sort((a, b) => a.time - b.time) } : l ); onUpdateTrack(track.id, { automation: { ...track.automation, lanes: updatedLanes }, }); }} onUpdatePoint={(pointId, updates) => { const updatedLanes = track.automation.lanes.map((l) => l.id === lane.id ? { ...l, points: l.points.map((p) => p.id === pointId ? { ...p, ...updates } : p ), } : l ); onUpdateTrack(track.id, { automation: { ...track.automation, lanes: updatedLanes }, }); }} onRemovePoint={(pointId) => { const updatedLanes = track.automation.lanes.map((l) => l.id === lane.id ? { ...l, points: l.points.filter((p) => p.id !== pointId) } : l ); onUpdateTrack(track.id, { automation: { ...track.automation, lanes: updatedLanes }, }); }} onUpdateLane={(updates) => { const updatedLanes = track.automation.lanes.map((l) => l.id === lane.id ? { ...l, ...updates } : l ); onUpdateTrack(track.id, { automation: { ...track.automation, lanes: updatedLanes }, }); }} /> ))}
)}
); })()} {/* Effects Bar */}
{/* Effects Header */}
Effects {/* Effect name labels */}
{track.effectChain.effects.map((effect) => ( {effect.name} ))}
{/* Add effect button - only visible when expanded */} {track.effectsExpanded && ( )} {/* Show/hide toggle */}
{/* Effects Content - Collapsible, horizontally scrollable */} {track.effectsExpanded && (
{track.effectChain.effects.length === 0 ? (
No effects. Click + to add an effect.
) : ( track.effectChain.effects.map((effect) => ( { const updatedChain = { ...track.effectChain, effects: track.effectChain.effects.map((e) => e.id === effect.id ? { ...e, enabled: !e.enabled } : e ), }; onUpdateTrack(track.id, { effectChain: updatedChain }); }} onRemove={() => { const updatedChain = { ...track.effectChain, effects: track.effectChain.effects.filter((e) => e.id !== effect.id), }; onUpdateTrack(track.id, { effectChain: updatedChain }); }} onUpdateParameters={(params) => { const updatedChain = { ...track.effectChain, effects: track.effectChain.effects.map((e) => e.id === effect.id ? { ...e, parameters: params } : e ), }; onUpdateTrack(track.id, { effectChain: updatedChain }); }} onToggleExpanded={() => { const updatedEffects = track.effectChain.effects.map((e) => e.id === effect.id ? { ...e, expanded: !e.expanded } : e ); onUpdateTrack(track.id, { effectChain: { ...track.effectChain, effects: updatedEffects }, }); }} /> )) )}
)}
)}
))}
{/* Desktop Layout - Two Column Layout */}
{/* Left Column: Track Controls (Fixed Width, No Scroll - synced with waveforms) */}
{tracks.map((track) => ( {/* Track Controls */} onSelectTrack(track.id) : undefined} onToggleMute={() => onUpdateTrack(track.id, { mute: !track.mute }) } onToggleSolo={() => onUpdateTrack(track.id, { solo: !track.solo }) } onToggleCollapse={() => onUpdateTrack(track.id, { collapsed: !track.collapsed }) } onVolumeChange={(volume) => onUpdateTrack(track.id, { volume }) } onPanChange={(pan) => onUpdateTrack(track.id, { pan }) } onRemove={() => onRemoveTrack(track.id)} onNameChange={(name) => onUpdateTrack(track.id, { name }) } onUpdateTrack={onUpdateTrack} onSeek={onSeek} onLoadAudio={(buffer) => onUpdateTrack(track.id, { audioBuffer: buffer }) } onToggleEffect={(effectId) => { const updatedChain = { ...track.effectChain, effects: track.effectChain.effects.map((e) => e.id === effectId ? { ...e, enabled: !e.enabled } : e ), }; onUpdateTrack(track.id, { effectChain: updatedChain }); }} onRemoveEffect={(effectId) => { const updatedChain = { ...track.effectChain, effects: track.effectChain.effects.filter((e) => e.id !== effectId), }; onUpdateTrack(track.id, { effectChain: updatedChain }); }} onUpdateEffect={(effectId, parameters) => { const updatedChain = { ...track.effectChain, effects: track.effectChain.effects.map((e) => e.id === effectId ? { ...e, parameters } : e ), }; onUpdateTrack(track.id, { effectChain: updatedChain }); }} onAddEffect={(effectType) => { const newEffect = createEffect( effectType, EFFECT_NAMES[effectType] ); const updatedChain = { ...track.effectChain, effects: [...track.effectChain.effects, newEffect], }; onUpdateTrack(track.id, { effectChain: updatedChain }); }} onSelectionChange={ onSelectionChange ? (selection) => onSelectionChange(track.id, selection) : undefined } onToggleRecordEnable={ onToggleRecordEnable ? () => onToggleRecordEnable(track.id) : undefined } isRecording={recordingTrackId === track.id} recordingLevel={recordingTrackId === track.id ? recordingLevel : 0} playbackLevel={trackLevels[track.id] || 0} onParameterTouched={onParameterTouched} isPlaying={isPlaying} renderControlsOnly={true} /> ))}
{/* Right Column: Waveforms (Flexible Width, Vertical Scroll Only) */}
{tracks.map((track) => ( {/* Track Waveform Row with bars stacked below - Total height matches track controls */}
{/* Waveform - Takes remaining space after bars */}
{/* Upload hint for empty tracks - stays fixed as overlay */} {!track.audioBuffer && !track.collapsed && (

Click waveform area to load audio

or drag & drop

)}
{ if (el) waveformHScrollRefs.current.set(track.id, el); }} onScroll={() => handleWaveformHScroll(track.id)} className="w-full h-full overflow-x-auto custom-scrollbar" >
= 1 ? `${duration * zoom * 5}px` : '100%', }} > onSelectTrack(track.id) : undefined} onToggleMute={() => onUpdateTrack(track.id, { mute: !track.mute }) } onToggleSolo={() => onUpdateTrack(track.id, { solo: !track.solo }) } onToggleCollapse={() => onUpdateTrack(track.id, { collapsed: !track.collapsed }) } onVolumeChange={(volume) => onUpdateTrack(track.id, { volume }) } onPanChange={(pan) => onUpdateTrack(track.id, { pan }) } onRemove={() => onRemoveTrack(track.id)} onNameChange={(name) => onUpdateTrack(track.id, { name }) } onUpdateTrack={onUpdateTrack} onSeek={onSeek} onLoadAudio={(buffer) => onUpdateTrack(track.id, { audioBuffer: buffer }) } onToggleEffect={(effectId) => { const updatedChain = { ...track.effectChain, effects: track.effectChain.effects.map((e) => e.id === effectId ? { ...e, enabled: !e.enabled } : e ), }; onUpdateTrack(track.id, { effectChain: updatedChain }); }} onRemoveEffect={(effectId) => { const updatedChain = { ...track.effectChain, effects: track.effectChain.effects.filter((e) => e.id !== effectId), }; onUpdateTrack(track.id, { effectChain: updatedChain }); }} onUpdateEffect={(effectId, parameters) => { const updatedChain = { ...track.effectChain, effects: track.effectChain.effects.map((e) => e.id === effectId ? { ...e, parameters } : e ), }; onUpdateTrack(track.id, { effectChain: updatedChain }); }} onAddEffect={(effectType) => { const newEffect = createEffect( effectType, EFFECT_NAMES[effectType] ); const updatedChain = { ...track.effectChain, effects: [...track.effectChain.effects, newEffect], }; onUpdateTrack(track.id, { effectChain: updatedChain }); }} onSelectionChange={ onSelectionChange ? (selection) => onSelectionChange(track.id, selection) : undefined } onToggleRecordEnable={ onToggleRecordEnable ? () => onToggleRecordEnable(track.id) : undefined } isRecording={recordingTrackId === track.id} recordingLevel={recordingTrackId === track.id ? recordingLevel : 0} playbackLevel={trackLevels[track.id] || 0} onParameterTouched={onParameterTouched} isPlaying={isPlaying} renderWaveformOnly={true} />
{/* Automation Bar - Collapsible - Fixed height when expanded */} {!track.collapsed && (() => { const selectedParam = track.automation.selectedParameterId || 'volume'; const currentLane = track.automation.lanes.find( l => l.parameterId === selectedParam ); // Build available parameters list const availableParameters: Array<{ id: string; name: string }> = [ { id: 'volume', name: 'Volume' }, { id: 'pan', name: 'Pan' }, ]; // Add effect parameters track.effectChain.effects.forEach((effect) => { if (effect.parameters) { Object.keys(effect.parameters).forEach((paramKey) => { const parameterId = `effect.${effect.id}.${paramKey}`; const paramName = `${effect.name} - ${paramKey.charAt(0).toUpperCase() + paramKey.slice(1)}`; availableParameters.push({ id: parameterId, name: paramName }); }); } }); // Get parameters that have automation lanes with points const automatedParams = track.automation.lanes .filter(lane => lane.points.length > 0) .map(lane => { const param = availableParameters.find(p => p.id === lane.parameterId); return param ? param.name : lane.parameterName; }); const modes = ['read', 'write', 'touch', 'latch'] as const; const MODE_LABELS = { read: 'R', write: 'W', touch: 'T', latch: 'L' }; const MODE_COLORS = { read: 'text-muted-foreground', write: 'text-red-500', touch: 'text-yellow-500', latch: 'text-orange-500', }; const currentModeIndex = modes.indexOf(currentLane?.mode || 'read'); return (
{/* Automation Header - Single Bar */}
Automation {/* Color indicator */} {currentLane?.color && (
)} {/* Parameter labels - always visible */}
{automatedParams.map((paramName, index) => ( {paramName} ))}
{/* Controls - only visible when expanded */} {track.automationExpanded && ( <> {/* Parameter selector */} {availableParameters && availableParameters.length > 1 && ( )} {/* Automation mode button */} {/* Height controls */}
)} {/* Show/hide toggle - Part of normal flow */}
{/* Automation Lane Content - Shown when expanded */} {track.automationExpanded && (
{ if (el) automationHScrollRefs.current.set(track.id, el); }} onScroll={() => handleAutomationHScroll(track.id)} className="overflow-x-auto custom-scrollbar" >
= 1 ? `${duration * zoom * 5}px` : '100%', }} > {track.automation.lanes .filter((lane) => lane.parameterId === (track.automation.selectedParameterId || 'volume') && lane.visible) .map((lane) => ( { const newPoint = createAutomationPoint({ time, value, curve: 'linear' }); const updatedLanes = track.automation.lanes.map((l) => l.id === lane.id ? { ...l, points: [...l.points, newPoint].sort((a, b) => a.time - b.time) } : l ); onUpdateTrack(track.id, { automation: { ...track.automation, lanes: updatedLanes }, }); }} onUpdatePoint={(pointId, updates) => { const updatedLanes = track.automation.lanes.map((l) => l.id === lane.id ? { ...l, points: l.points.map((p) => p.id === pointId ? { ...p, ...updates } : p ), } : l ); onUpdateTrack(track.id, { automation: { ...track.automation, lanes: updatedLanes }, }); }} onRemovePoint={(pointId) => { const updatedLanes = track.automation.lanes.map((l) => l.id === lane.id ? { ...l, points: l.points.filter((p) => p.id !== pointId) } : l ); onUpdateTrack(track.id, { automation: { ...track.automation, lanes: updatedLanes }, }); }} onUpdateLane={(updates) => { const updatedLanes = track.automation.lanes.map((l) => l.id === lane.id ? { ...l, ...updates } : l ); onUpdateTrack(track.id, { automation: { ...track.automation, lanes: updatedLanes }, }); }} /> ))}
)}
); })()} {/* Effects Bar - Collapsible - Fixed height when expanded */} {!track.collapsed && (
{/* Effects Header - Collapsible */}
Effects {/* Effect name labels */}
{track.effectChain.effects.map((effect) => ( {effect.name} ))}
{/* Add effect button - only visible when expanded */} {track.effectsExpanded && ( )} {/* Show/hide toggle */}
{/* Effects Content - Collapsible, horizontally scrollable */} {track.effectsExpanded && (
{track.effectChain.effects.length === 0 ? (
No effects. Click + to add an effect.
) : ( track.effectChain.effects.map((effect) => ( { const updatedChain = { ...track.effectChain, effects: track.effectChain.effects.map((e) => e.id === effect.id ? { ...e, enabled: !e.enabled } : e ), }; onUpdateTrack(track.id, { effectChain: updatedChain }); }} onRemove={() => { const updatedChain = { ...track.effectChain, effects: track.effectChain.effects.filter((e) => e.id !== effect.id), }; onUpdateTrack(track.id, { effectChain: updatedChain }); }} onUpdateParameters={(params) => { const updatedChain = { ...track.effectChain, effects: track.effectChain.effects.map((e) => e.id === effect.id ? { ...e, parameters: params } : e ), }; onUpdateTrack(track.id, { effectChain: updatedChain }); }} onToggleExpanded={() => { const updatedEffects = track.effectChain.effects.map((e) => e.id === effect.id ? { ...e, expanded: !e.expanded } : e ); onUpdateTrack(track.id, { effectChain: { ...track.effectChain, effects: updatedEffects }, }); }} /> )) )}
)}
)}
))}
{/* Import Dialog */} {onImportTrack && ( setImportDialogOpen(false)} onImportTrack={handleImportTrack} /> )} {/* Effect Browser Dialog */} setEffectBrowserTrackId(null)} onSelectEffect={(effectType) => { if (effectBrowserTrackId) { const track = tracks.find((t) => t.id === effectBrowserTrackId); if (track) { const newEffect = createEffect(effectType, EFFECT_NAMES[effectType]); const updatedChain = { ...track.effectChain, effects: [...track.effectChain.effects, newEffect], }; onUpdateTrack(effectBrowserTrackId, { effectChain: updatedChain }); } } }} />
); }