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
113 lines
3.3 KiB
TypeScript
113 lines
3.3 KiB
TypeScript
'use client';
|
|
|
|
import { useCallback, useState } from 'react';
|
|
import { Plus } from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { SectionBlock } from './SectionBlock';
|
|
import { SectionPicker } from './SectionPicker';
|
|
import { cn } from '@/lib/utils';
|
|
import { Section, SectionType } from '@/types/audio';
|
|
|
|
interface SectionTrackProps {
|
|
sections: Section[];
|
|
durationBars: number;
|
|
pixelsPerBar: number;
|
|
selectedId: string | null;
|
|
onSelect: (id: string | null) => void;
|
|
onAdd: (type: SectionType, startBar: number, endBar: number) => void;
|
|
onResize: (id: string, startBar: number, endBar: number) => void;
|
|
onMove: (id: string, startBar: number) => void;
|
|
onDelete: (id: string) => void;
|
|
className?: string;
|
|
}
|
|
|
|
export function SectionTrack({
|
|
sections,
|
|
durationBars,
|
|
pixelsPerBar,
|
|
selectedId,
|
|
onSelect,
|
|
onAdd,
|
|
onResize,
|
|
onMove,
|
|
onDelete,
|
|
className,
|
|
}: SectionTrackProps) {
|
|
const [showPicker, setShowPicker] = useState(false);
|
|
const [pickerPosition, setPickerPosition] = useState({ x: 0, bar: 0 });
|
|
|
|
const handleTrackClick = useCallback(
|
|
(e: React.MouseEvent<HTMLDivElement>) => {
|
|
const rect = e.currentTarget.getBoundingClientRect();
|
|
const x = e.clientX - rect.left;
|
|
const bar = Math.floor(x / pixelsPerBar);
|
|
|
|
if (bar >= 0 && bar < durationBars) {
|
|
setPickerPosition({ x, bar });
|
|
setShowPicker(true);
|
|
}
|
|
},
|
|
[pixelsPerBar, durationBars]
|
|
);
|
|
|
|
const handleSelectType = useCallback(
|
|
(type: SectionType) => {
|
|
const endBar = Math.min(durationBars, pickerPosition.bar + 4);
|
|
onAdd(type, pickerPosition.bar, endBar);
|
|
},
|
|
[pickerPosition.bar, durationBars, onAdd]
|
|
);
|
|
|
|
return (
|
|
<div className={cn('relative', className)}>
|
|
<div className="absolute left-0 top-0 bottom-0 w-20 flex items-center gap-1.5 px-2 bg-card/50 border-r border-border/50 z-10">
|
|
<Plus className="h-3 w-3 text-muted-foreground" />
|
|
<span className="text-xs font-medium text-muted-foreground">Sections</span>
|
|
</div>
|
|
|
|
<div
|
|
className="ml-20 h-8 bg-muted/10 border-b border-border/30 cursor-pointer relative"
|
|
style={{ width: durationBars * pixelsPerBar }}
|
|
onClick={handleTrackClick}
|
|
>
|
|
{Array.from({ length: durationBars }, (_, i) => (
|
|
<div
|
|
key={i}
|
|
className={cn(
|
|
'absolute top-0 bottom-0 border-l',
|
|
i % 4 === 0 ? 'border-border/30' : 'border-border/10'
|
|
)}
|
|
style={{ left: i * pixelsPerBar }}
|
|
/>
|
|
))}
|
|
|
|
{sections.map((section) => (
|
|
<SectionBlock
|
|
key={section.id}
|
|
section={section}
|
|
pixelsPerBar={pixelsPerBar}
|
|
durationBars={durationBars}
|
|
onResize={onResize}
|
|
onMove={onMove}
|
|
onSelect={onSelect}
|
|
onDelete={onDelete}
|
|
selected={section.id === selectedId}
|
|
/>
|
|
))}
|
|
|
|
{showPicker && (
|
|
<div
|
|
className="absolute top-full mt-1"
|
|
style={{ left: Math.min(pickerPosition.x, durationBars * pixelsPerBar - 128) }}
|
|
>
|
|
<SectionPicker
|
|
onSelect={handleSelectType}
|
|
onClose={() => setShowPicker(false)}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|