work in progress implementation of: - mixer with channel strips, faders, pan knobs, level meters - timeline with ruler, playhead, sections, keyframe tracks - pattern and progression pickers for drums/chords - automation lanes and mute tracks - loop bracket for loop region selection - export modal placeholder known issues: - drum pattern changes don't update audio engine - timeline keyframes not connected to scheduler - some UI bugs remain this is a checkpoint commit for further iteration
131 lines
3.8 KiB
TypeScript
131 lines
3.8 KiB
TypeScript
'use client';
|
|
|
|
import { useCallback, useRef, useState } from 'react';
|
|
import { cn } from '@/lib/utils';
|
|
import { Section, SECTION_COLORS } from '@/types/audio';
|
|
|
|
interface SectionBlockProps {
|
|
section: Section;
|
|
pixelsPerBar: number;
|
|
durationBars: number;
|
|
onResize: (id: string, startBar: number, endBar: number) => void;
|
|
onMove: (id: string, startBar: number) => void;
|
|
onSelect: (id: string) => void;
|
|
onDelete: (id: string) => void;
|
|
selected?: boolean;
|
|
}
|
|
|
|
export function SectionBlock({
|
|
section,
|
|
pixelsPerBar,
|
|
durationBars,
|
|
onResize,
|
|
onMove,
|
|
onSelect,
|
|
onDelete,
|
|
selected = false,
|
|
}: SectionBlockProps) {
|
|
const [dragging, setDragging] = useState<'move' | 'start' | 'end' | null>(null);
|
|
const dragStart = useRef({ x: 0, startBar: 0, endBar: 0 });
|
|
|
|
const left = section.startBar * pixelsPerBar;
|
|
const width = (section.endBar - section.startBar) * pixelsPerBar;
|
|
const color = SECTION_COLORS[section.type];
|
|
|
|
const handleMouseDown = useCallback(
|
|
(type: 'move' | 'start' | 'end', e: React.MouseEvent) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
setDragging(type);
|
|
dragStart.current = {
|
|
x: e.clientX,
|
|
startBar: section.startBar,
|
|
endBar: section.endBar,
|
|
};
|
|
onSelect(section.id);
|
|
|
|
const handleMouseMove = (e: MouseEvent) => {
|
|
const deltaX = e.clientX - dragStart.current.x;
|
|
const deltaBars = Math.round(deltaX / pixelsPerBar);
|
|
const { startBar, endBar } = dragStart.current;
|
|
|
|
if (type === 'start') {
|
|
const newStart = Math.max(0, Math.min(endBar - 1, startBar + deltaBars));
|
|
onResize(section.id, newStart, endBar);
|
|
} else if (type === 'end') {
|
|
const newEnd = Math.max(startBar + 1, Math.min(durationBars, endBar + deltaBars));
|
|
onResize(section.id, startBar, newEnd);
|
|
} else {
|
|
const duration = endBar - startBar;
|
|
const newStart = Math.max(
|
|
0,
|
|
Math.min(durationBars - duration, startBar + deltaBars)
|
|
);
|
|
onMove(section.id, newStart);
|
|
}
|
|
};
|
|
|
|
const handleMouseUp = () => {
|
|
setDragging(null);
|
|
document.removeEventListener('mousemove', handleMouseMove);
|
|
document.removeEventListener('mouseup', handleMouseUp);
|
|
};
|
|
|
|
document.addEventListener('mousemove', handleMouseMove);
|
|
document.addEventListener('mouseup', handleMouseUp);
|
|
},
|
|
[section, pixelsPerBar, durationBars, onResize, onMove, onSelect]
|
|
);
|
|
|
|
const handleContextMenu = useCallback(
|
|
(e: React.MouseEvent) => {
|
|
e.preventDefault();
|
|
onDelete(section.id);
|
|
},
|
|
[section.id, onDelete]
|
|
);
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'absolute top-1 bottom-1 rounded-md cursor-move',
|
|
'transition-shadow',
|
|
selected && 'ring-2 ring-white/50 shadow-lg'
|
|
)}
|
|
style={{
|
|
left,
|
|
width,
|
|
backgroundColor: color,
|
|
}}
|
|
onContextMenu={handleContextMenu}
|
|
>
|
|
<div
|
|
className={cn(
|
|
'absolute left-0 top-0 bottom-0 w-2 cursor-ew-resize',
|
|
'hover:bg-white/20 rounded-l-md',
|
|
dragging === 'start' && 'bg-white/30'
|
|
)}
|
|
onMouseDown={(e) => handleMouseDown('start', e)}
|
|
/>
|
|
|
|
<div
|
|
className="absolute inset-x-2 inset-y-0 flex items-center px-1 overflow-hidden"
|
|
onMouseDown={(e) => handleMouseDown('move', e)}
|
|
>
|
|
<span className="text-[10px] font-medium text-white/90 truncate">
|
|
{section.name || section.type}
|
|
</span>
|
|
</div>
|
|
|
|
<div
|
|
className={cn(
|
|
'absolute right-0 top-0 bottom-0 w-2 cursor-ew-resize',
|
|
'hover:bg-white/20 rounded-r-md',
|
|
dragging === 'end' && 'bg-white/30'
|
|
)}
|
|
onMouseDown={(e) => handleMouseDown('end', e)}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|