Lofi_Generator/components/mixer/VerticalFader.tsx
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

114 lines
2.9 KiB
TypeScript

'use client';
import { useCallback, useRef, useState } from 'react';
import { cn } from '@/lib/utils';
interface VerticalFaderProps {
value: number;
onChange: (value: number) => void;
min?: number;
max?: number;
height?: number;
className?: string;
}
export function VerticalFader({
value,
onChange,
min = 0,
max = 1,
height = 64,
className,
}: VerticalFaderProps) {
const trackRef = useRef<HTMLDivElement>(null);
const [isDragging, setIsDragging] = useState(false);
const normalizedValue = (value - min) / (max - min);
const handleMove = useCallback(
(clientY: number) => {
if (!trackRef.current) return;
const rect = trackRef.current.getBoundingClientRect();
const y = clientY - rect.top;
const percentage = 1 - Math.max(0, Math.min(1, y / rect.height));
const newValue = min + percentage * (max - min);
onChange(newValue);
},
[min, max, onChange]
);
const handleMouseDown = useCallback(
(e: React.MouseEvent) => {
e.preventDefault();
setIsDragging(true);
handleMove(e.clientY);
const handleMouseMove = (e: MouseEvent) => {
handleMove(e.clientY);
};
const handleMouseUp = () => {
setIsDragging(false);
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleMouseUp);
};
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseup', handleMouseUp);
},
[handleMove]
);
const handleTouchStart = useCallback(
(e: React.TouchEvent) => {
setIsDragging(true);
handleMove(e.touches[0].clientY);
const handleTouchMove = (e: TouchEvent) => {
handleMove(e.touches[0].clientY);
};
const handleTouchEnd = () => {
setIsDragging(false);
window.removeEventListener('touchmove', handleTouchMove);
window.removeEventListener('touchend', handleTouchEnd);
};
window.addEventListener('touchmove', handleTouchMove);
window.addEventListener('touchend', handleTouchEnd);
},
[handleMove]
);
return (
<div
ref={trackRef}
className={cn(
'relative w-3 rounded-full cursor-pointer select-none',
'bg-border/60',
className
)}
style={{ height }}
onMouseDown={handleMouseDown}
onTouchStart={handleTouchStart}
>
{/* Fill */}
<div
className="absolute bottom-0 left-0 right-0 rounded-full bg-primary transition-all duration-75"
style={{ height: `${normalizedValue * 100}%` }}
/>
{/* Thumb */}
<div
className={cn(
'absolute left-1/2 -translate-x-1/2 w-4 h-4 rounded-full',
'bg-white border-2 border-primary shadow-md',
'transition-transform duration-75',
isDragging && 'scale-110'
)}
style={{ bottom: `calc(${normalizedValue * 100}% - 8px)` }}
/>
</div>
);
}