Nicholai d3158e4c6a feat(timeline-mixer): WIP timeline and mixer components
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
2026-01-20 18:22:10 -07:00

82 lines
2.4 KiB
TypeScript

'use client';
import { useCallback, useRef, useState } from 'react';
import { cn } from '@/lib/utils';
interface PanKnobProps {
value: number;
onChange: (value: number) => void;
className?: string;
size?: number;
}
export function PanKnob({ value, onChange, className, size = 32 }: PanKnobProps) {
const knobRef = useRef<HTMLDivElement>(null);
const [isDragging, setIsDragging] = useState(false);
const startY = useRef(0);
const startValue = useRef(0);
const rotation = value * 135;
const handleMouseDown = useCallback(
(e: React.MouseEvent) => {
e.preventDefault();
setIsDragging(true);
startY.current = e.clientY;
startValue.current = value;
const handleMouseMove = (e: MouseEvent) => {
const delta = (startY.current - e.clientY) / 100;
const newValue = Math.max(-1, Math.min(1, startValue.current + delta));
onChange(newValue);
};
const handleMouseUp = () => {
setIsDragging(false);
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
},
[value, onChange]
);
const handleDoubleClick = useCallback(() => {
onChange(0);
}, [onChange]);
const label = value === 0 ? 'C' : value < 0 ? 'L' : 'R';
return (
<div className={cn('flex flex-col items-center gap-1', className)}>
<div
ref={knobRef}
className={cn(
'relative rounded-full bg-muted border-2 border-border cursor-ns-resize',
'hover:border-primary/50 transition-colors',
isDragging && 'border-primary'
)}
style={{ width: size, height: size }}
onMouseDown={handleMouseDown}
onDoubleClick={handleDoubleClick}
>
<div
className="absolute top-1 left-1/2 w-0.5 h-2 bg-primary rounded-full -translate-x-1/2 origin-bottom"
style={{
transform: `translateX(-50%) rotate(${rotation}deg)`,
transformOrigin: `50% ${size / 2 - 4}px`,
}}
/>
<div
className="absolute inset-0 flex items-center justify-center text-[8px] font-mono text-muted-foreground"
>
{label}
</div>
</div>
<span className="text-[10px] text-muted-foreground uppercase">Pan</span>
</div>
);
}