'use client'; import { useCallback, useMemo, useRef, useState } from 'react'; import { cn } from '@/lib/utils'; import { AutomationPoint } from '@/types/audio'; interface AutomationLaneProps { label: string; icon?: React.ReactNode; points: AutomationPoint[]; durationBars: number; pixelsPerBar: number; height?: number; onAddPoint: (bar: number, beat: number, value: number) => void; onUpdatePoint: (id: string, bar: number, beat: number, value: number) => void; onDeletePoint: (id: string) => void; className?: string; } export function AutomationLane({ label, icon, points, durationBars, pixelsPerBar, height = 48, onAddPoint, onUpdatePoint, onDeletePoint, className, }: AutomationLaneProps) { const containerRef = useRef(null); const [draggingId, setDraggingId] = useState(null); const [selectedId, setSelectedId] = useState(null); const pixelsPerBeat = pixelsPerBar / 4; const width = durationBars * pixelsPerBar; const sortedPoints = useMemo(() => { return [...points].sort((a, b) => { if (a.bar !== b.bar) return a.bar - b.bar; return a.beat - b.beat; }); }, [points]); const pathD = useMemo(() => { if (sortedPoints.length === 0) return ''; const toX = (bar: number, beat: number) => bar * pixelsPerBar + beat * pixelsPerBeat; const toY = (value: number) => height - value * height; let d = `M 0 ${toY(sortedPoints[0]?.value ?? 0.5)}`; if (sortedPoints.length > 0) { d = `M ${toX(sortedPoints[0].bar, sortedPoints[0].beat)} ${toY(sortedPoints[0].value)}`; for (let i = 1; i < sortedPoints.length; i++) { const p = sortedPoints[i]; d += ` L ${toX(p.bar, p.beat)} ${toY(p.value)}`; } } return d; }, [sortedPoints, pixelsPerBar, pixelsPerBeat, height]); const handleContainerClick = useCallback( (e: React.MouseEvent) => { if (draggingId) return; const rect = e.currentTarget.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; const totalBeats = x / pixelsPerBeat; const bar = Math.floor(totalBeats / 4); const beat = Math.floor(totalBeats % 4); const value = Math.max(0, Math.min(1, 1 - y / height)); if (bar >= 0 && bar < durationBars) { onAddPoint(bar, beat, value); } }, [draggingId, pixelsPerBeat, height, durationBars, onAddPoint] ); const handlePointMouseDown = useCallback( (point: AutomationPoint, e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); setDraggingId(point.id); setSelectedId(point.id); const handleMouseMove = (e: MouseEvent) => { if (!containerRef.current) return; const rect = containerRef.current.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; const totalBeats = Math.max(0, x / pixelsPerBeat); const bar = Math.min(durationBars - 1, Math.floor(totalBeats / 4)); const beat = Math.floor(totalBeats % 4); const value = Math.max(0, Math.min(1, 1 - y / height)); onUpdatePoint(point.id, bar, beat, value); }; const handleMouseUp = () => { setDraggingId(null); document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }; document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); }, [pixelsPerBeat, height, durationBars, onUpdatePoint] ); const handlePointContextMenu = useCallback( (point: AutomationPoint, e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); onDeletePoint(point.id); }, [onDeletePoint] ); return (
{icon} {label}
{Array.from({ length: durationBars }, (_, i) => (
))} {pathD && ( )} {sortedPoints.map((point) => { const x = point.bar * pixelsPerBar + point.beat * pixelsPerBeat; const y = height - point.value * height; return (
handlePointMouseDown(point, e)} onContextMenu={(e) => handlePointContextMenu(point, e)} /> ); })}
); }