import { useDroppable, useDraggable } from '@dnd-kit/core' import type { Track, PlacedSound } from '@/pages/SequencerPage' import { Button } from '@/components/ui/button' import { Trash2, Volume2 } from 'lucide-react' import { useState, forwardRef, useRef, useEffect } from 'react' interface SequencerCanvasProps { tracks: Track[] duration: number zoom: number currentTime: number isPlaying: boolean onScroll?: () => void draggedItem?: any // Current dragged item from parent dragOverInfo?: {trackId: string, x: number} | null // Drag over position info onRemoveSound: (soundId: string, trackId: string) => void timeIntervals: {minorIntervals: number[], majorIntervals: number[], minorInterval: number, majorInterval: number} } interface TrackRowProps { track: Track duration: number zoom: number isPlaying: boolean currentTime: number draggedItem?: any // Current dragged item dragOverInfo?: {trackId: string, x: number} | null // Drag over position info onRemoveSound: (soundId: string, trackId: string) => void timeIntervals: {minorIntervals: number[], majorIntervals: number[], minorInterval: number, majorInterval: number} } interface PlacedSoundItemProps { sound: PlacedSound zoom: number trackId: string onRemove: (soundId: string) => void minorInterval: number } function PlacedSoundItem({ sound, zoom, trackId, onRemove, minorInterval }: PlacedSoundItemProps) { const { attributes, listeners, setNodeRef, transform, isDragging, } = useDraggable({ id: sound.id, data: { type: 'placed-sound', ...sound, trackId, }, }) const style = transform ? { transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`, } : undefined const width = (sound.duration / 1000) * zoom // Convert ms to seconds for zoom calculation // Ensure placed sounds are positioned at snapped locations using current minor interval const startTimeSeconds = sound.startTime / 1000 const snappedStartTime = Math.round(startTimeSeconds / minorInterval) * minorInterval const left = Math.max(0, snappedStartTime) * zoom const formatTime = (seconds: number): string => { const mins = Math.floor(seconds / 60) const secs = Math.floor(seconds % 60) return `${mins}:${secs.toString().padStart(2, '0')}` } return (
{sound.name}
) } function TrackRow({ track, duration, zoom, isPlaying, currentTime, draggedItem, dragOverInfo, onRemoveSound, timeIntervals }: TrackRowProps) { const playheadPosition = (currentTime / 1000) * zoom // Convert ms to seconds for zoom calculation const { isOver, setNodeRef: setDropRef } = useDroppable({ id: `track-${track.id}`, data: { type: 'track', trackId: track.id, }, }) const handleRemoveSound = (soundId: string) => { onRemoveSound(soundId, track.id) } const { minorIntervals, majorIntervals } = timeIntervals return (
{/* Grid lines for time markers */}
{/* Minor grid lines */} {minorIntervals.map((time) => (
))} {/* Major grid lines */} {majorIntervals.map((time) => (
))}
{/* Precise drag preview (dragOverInfo.x is already snapped) */} {draggedItem && dragOverInfo && dragOverInfo.trackId === track.id && (() => { const soundDurationMs = draggedItem.type === 'sound' ? draggedItem.sound.duration // Already in ms : draggedItem.duration // Already in ms const soundDurationSeconds = soundDurationMs / 1000 // dragOverInfo.x is already snapped in the parent component const startTimeSeconds = dragOverInfo.x / zoom const endTimeSeconds = startTimeSeconds + soundDurationSeconds const durationSeconds = duration / 1000 const isValidPosition = startTimeSeconds >= 0 && endTimeSeconds <= durationSeconds return (
{draggedItem.type === 'sound' ? (draggedItem.sound.name || draggedItem.sound.filename) : draggedItem.name } {!isValidPosition && ' (Invalid)'}
) })()} {/* Playhead */} {isPlaying && (
)} {/* Placed sounds */} {track.sounds.map((sound) => ( ))}
) } export const SequencerCanvas = forwardRef(({ tracks, duration, zoom, currentTime, isPlaying, onScroll, draggedItem, dragOverInfo, onRemoveSound, timeIntervals, }, ref) => { const totalWidth = (duration / 1000) * zoom // Convert ms to seconds for zoom calculation const timelineRef = useRef(null) // Add a fallback droppable for the entire canvas area const { setNodeRef: setCanvasDropRef } = useDroppable({ id: 'sequencer-canvas', data: { type: 'canvas', }, }) const { minorIntervals, majorIntervals } = timeIntervals const handleTracksScroll = (e: React.UIEvent) => { // Sync timeline horizontal scroll with tracks if (timelineRef.current) { const scrollLeft = e.currentTarget.scrollLeft // Only update if different to prevent scroll fighting if (Math.abs(timelineRef.current.scrollLeft - scrollLeft) > 1) { timelineRef.current.scrollLeft = scrollLeft } } // Call the original scroll handler for vertical sync onScroll?.() } return (
{/* Time ruler */}
{/* Minor time markers */} {minorIntervals.map((time) => (
))} {/* Major time markers with labels */} {majorIntervals.map((time) => { const formatTime = (seconds: number): string => { if (seconds < 60) { // For times under 1 minute, show seconds with decimal places if needed return seconds < 10 && seconds % 1 !== 0 ? seconds.toFixed(1) + 's' : Math.floor(seconds) + 's' } else { // For times over 1 minute, show MM:SS format const mins = Math.floor(seconds / 60) const secs = Math.floor(seconds % 60) return `${mins}:${secs.toString().padStart(2, '0')}` } } return (
{formatTime(time)}
) })} {/* Playhead in ruler */} {isPlaying && (
)}
{/* Tracks */}
{tracks.map((track) => ( ))}
) }) SequencerCanvas.displayName = 'SequencerCanvas'