forked from averyfelts/Lofi_Generator
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
128 lines
3.8 KiB
TypeScript
128 lines
3.8 KiB
TypeScript
'use client';
|
|
|
|
import { useCallback, useRef, useState } from 'react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Repeat } from 'lucide-react';
|
|
import { cn } from '@/lib/utils';
|
|
import { LoopRegion } from '@/types/audio';
|
|
|
|
interface LoopBracketProps {
|
|
region: LoopRegion;
|
|
durationBars: number;
|
|
pixelsPerBar: number;
|
|
onChange: (region: LoopRegion) => void;
|
|
className?: string;
|
|
}
|
|
|
|
export function LoopBracket({
|
|
region,
|
|
durationBars,
|
|
pixelsPerBar,
|
|
onChange,
|
|
className,
|
|
}: LoopBracketProps) {
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
const [dragging, setDragging] = useState<'start' | 'end' | 'region' | null>(null);
|
|
const dragStart = useRef({ x: 0, region: { ...region } });
|
|
|
|
const left = region.start * pixelsPerBar;
|
|
const width = (region.end - region.start) * pixelsPerBar;
|
|
|
|
const handleMouseDown = useCallback(
|
|
(type: 'start' | 'end' | 'region', e: React.MouseEvent) => {
|
|
e.preventDefault();
|
|
setDragging(type);
|
|
dragStart.current = { x: e.clientX, region: { ...region } };
|
|
|
|
const handleMouseMove = (e: MouseEvent) => {
|
|
if (!containerRef.current) return;
|
|
const deltaX = e.clientX - dragStart.current.x;
|
|
const deltaBars = Math.round(deltaX / pixelsPerBar);
|
|
const { start, end, enabled } = dragStart.current.region;
|
|
|
|
let newStart = start;
|
|
let newEnd = end;
|
|
|
|
if (type === 'start') {
|
|
newStart = Math.max(0, Math.min(end - 1, start + deltaBars));
|
|
} else if (type === 'end') {
|
|
newEnd = Math.max(start + 1, Math.min(durationBars, end + deltaBars));
|
|
} else if (type === 'region') {
|
|
const duration = end - start;
|
|
newStart = Math.max(0, Math.min(durationBars - duration, start + deltaBars));
|
|
newEnd = newStart + duration;
|
|
}
|
|
|
|
onChange({ start: newStart, end: newEnd, enabled });
|
|
};
|
|
|
|
const handleMouseUp = () => {
|
|
setDragging(null);
|
|
document.removeEventListener('mousemove', handleMouseMove);
|
|
document.removeEventListener('mouseup', handleMouseUp);
|
|
};
|
|
|
|
document.addEventListener('mousemove', handleMouseMove);
|
|
document.addEventListener('mouseup', handleMouseUp);
|
|
},
|
|
[region, pixelsPerBar, durationBars, onChange]
|
|
);
|
|
|
|
const toggleEnabled = useCallback(() => {
|
|
onChange({ ...region, enabled: !region.enabled });
|
|
}, [region, onChange]);
|
|
|
|
return (
|
|
<div
|
|
ref={containerRef}
|
|
className={cn('relative h-5', className)}
|
|
style={{ width: durationBars * pixelsPerBar }}
|
|
>
|
|
<div
|
|
className={cn(
|
|
'absolute top-0 h-full rounded-sm transition-colors',
|
|
region.enabled
|
|
? 'bg-lofi-orange/20 border border-lofi-orange/50'
|
|
: 'bg-muted/20 border border-border/50'
|
|
)}
|
|
style={{ left, width }}
|
|
>
|
|
<div
|
|
className={cn(
|
|
'absolute left-0 top-0 bottom-0 w-2 cursor-ew-resize',
|
|
'hover:bg-lofi-orange/30',
|
|
dragging === 'start' && 'bg-lofi-orange/40'
|
|
)}
|
|
onMouseDown={(e) => handleMouseDown('start', e)}
|
|
/>
|
|
|
|
<div
|
|
className="absolute inset-x-2 inset-y-0 cursor-move"
|
|
onMouseDown={(e) => handleMouseDown('region', e)}
|
|
/>
|
|
|
|
<div
|
|
className={cn(
|
|
'absolute right-0 top-0 bottom-0 w-2 cursor-ew-resize',
|
|
'hover:bg-lofi-orange/30',
|
|
dragging === 'end' && 'bg-lofi-orange/40'
|
|
)}
|
|
onMouseDown={(e) => handleMouseDown('end', e)}
|
|
/>
|
|
</div>
|
|
|
|
<Button
|
|
variant={region.enabled ? 'default' : 'outline'}
|
|
size="sm"
|
|
className={cn(
|
|
'absolute -right-8 top-0 h-5 w-6 p-0',
|
|
region.enabled && 'bg-lofi-orange hover:bg-lofi-orange/80'
|
|
)}
|
|
onClick={toggleEnabled}
|
|
>
|
|
<Repeat className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
);
|
|
}
|