import React, { useRef, useState, useEffect } from 'react'; import { INSTRUMENTS, BASS_NOTES, MIN_TEMPO, MAX_TEMPO, MIN_STEPS, MAX_STEPS } from '../constants'; import { PlayIcon, StopIcon, ClearIcon, UploadIcon, DownloadIcon, PlusIcon, MinusIcon, MuteIcon, UnmuteIcon } from './icons'; import { Grid, BassLineGrid } from '../types'; import { Modal } from './Modal'; interface SequencerProps { steps: number; grid: Grid; bassLine: BassLineGrid; isPlaying: boolean; tempo: number; currentStep: number | null; mutes: boolean[]; drumVolume: number; bassVolume: number; setStep: (instrumentIndex: number, stepIndex: number, isActive: boolean) => void; setBassNote: (stepIndex: number, note: string) => void; clearPattern: () => void; setTempo: React.Dispatch>; handleStepsChange: (newSteps: number) => void; startPlayback: () => Promise; stopPlayback: () => void; exportBeat: () => void; importBeat: (event: React.ChangeEvent) => void; toggleMute: (instrumentIndex: number) => void; handleDrumVolumeChange: (newVolume: number) => void; handleBassVolumeChange: (newVolume: number) => void; triggerSound: (instrumentName: string) => void; triggerBassNote: (note: string) => void; } const SequencerButton: React.FC<{ onClick?: () => void; children: React.ReactNode; className?: string; disabled?: boolean }> = ({ onClick, children, className, disabled }) => ( ); export const Sequencer: React.FC = ({ steps, grid, isPlaying, tempo, currentStep, mutes, bassLine, drumVolume, bassVolume, clearPattern, setTempo, handleStepsChange, setStep, setBassNote, startPlayback, stopPlayback, exportBeat, importBeat, toggleMute, handleDrumVolumeChange, handleBassVolumeChange, triggerSound, triggerBassNote }) => { const fileInputRef = useRef(null); const [isDragging, setIsDragging] = useState(false); const [dragActivationState, setDragActivationState] = useState(false); const [dragStartRow, setDragStartRow] = useState(null); const [isBassDragging, setIsBassDragging] = useState(false); const [bassDragActivationState, setBassDragActivationState] = useState(false); const [isClearModalOpen, setIsClearModalOpen] = useState(false); useEffect(() => { const handleMouseUp = () => { setIsDragging(false); setDragStartRow(null); setIsBassDragging(false); }; window.addEventListener('mouseup', handleMouseUp); return () => window.removeEventListener('mouseup', handleMouseUp); }, []); const handleImportClick = () => { fileInputRef.current?.click(); }; const handleCellMouseDown = (instIndex: number, stepIndex: number) => { setIsDragging(true); setDragStartRow(instIndex); const newState = !grid[instIndex]?.[stepIndex]; setDragActivationState(newState); setStep(instIndex, stepIndex, newState); if (newState && !isPlaying) { triggerSound(INSTRUMENTS[instIndex].name); } }; const handleCellMouseEnter = (instIndex: number, stepIndex: number) => { if (isDragging && instIndex === dragStartRow) { setStep(instIndex, stepIndex, dragActivationState); } }; const handleBassCellMouseDown = (noteName: string, stepIndex: number) => { const isCurrentlyActive = bassLine[stepIndex]?.includes(noteName); const newState = !isCurrentlyActive; setBassNote(stepIndex, noteName); setIsBassDragging(true); setBassDragActivationState(newState); if (newState && !isPlaying) { triggerBassNote(noteName); } }; const handleBassCellMouseEnter = (noteName: string, stepIndex: number) => { if (isBassDragging) { const isCurrentlyActive = bassLine[stepIndex]?.includes(noteName); if (bassDragActivationState !== isCurrentlyActive) { setBassNote(stepIndex, noteName); } } }; const incrementTempo = () => setTempo(t => Math.min(MAX_TEMPO, t + 1)); const decrementTempo = () => setTempo(t => Math.max(MIN_TEMPO, t - 1)); const handleTempoScroll = (e: React.WheelEvent) => { e.preventDefault(); if (e.deltaY < 0) { decrementTempo(); } else { incrementTempo(); } }; const incrementSteps = () => { const newSteps = steps + 4; if (newSteps <= MAX_STEPS) { handleStepsChange(newSteps); } }; const decrementSteps = () => { const newSteps = steps - 4; if (newSteps >= MIN_STEPS) { handleStepsChange(newSteps); } }; const handleClearConfirm = () => { clearPattern(); setIsClearModalOpen(false); }; return ( <> setIsClearModalOpen(false)} onConfirm={handleClearConfirm} title="Clear Session" >

Are you sure you want to clear the session? This will erase all current progress.

{!isPlaying ? ( Play ) : ( Stop )} setIsClearModalOpen(true)} title="Clear"> Clear
{tempo} BPM
{steps}
Import Export
handleDrumVolumeChange(Number(e.target.value))} className="w-4 h-48 appearance-none cursor-pointer bg-slate-200 rounded-lg [writing-mode:vertical-lr] [direction:rtl] accent-orange-500" aria-orientation="vertical" />
{/* Header */}
{Array.from({ length: steps }, (_, i) => (
{(i % 4 === 0) ? (i/4 + 1) : ''}
))} {/* Grid Rows */} {INSTRUMENTS.map((instrument, instIndex) => (
{instrument.name}
{grid[instIndex]?.map((isActive, stepIndex) => { const isCurrent = currentStep === stepIndex; const isFourth = stepIndex % 4 === 0; return (
handleCellMouseDown(instIndex, stepIndex)} onMouseEnter={() => handleCellMouseEnter(instIndex, stepIndex)} onDragStart={(e) => e.preventDefault()} /> ) })}
))}
{/* Bass Sequencer */}
handleBassVolumeChange(Number(e.target.value))} className="w-4 h-48 appearance-none cursor-pointer bg-slate-200 rounded-lg [writing-mode:vertical-lr] [direction:rtl] accent-purple-500" aria-orientation="vertical" />
{/* Header */}
{Array.from({ length: steps }, (_, i) => (
{(i % 4 === 0) ? {(i/4 + 1)} : ''}
))} {/* Bass Grid Rows (reversed to have low notes at the bottom) */} {[...BASS_NOTES].reverse().map((note) => (
{note.name}
{Array.from({ length: steps }).map((_, stepIndex) => { const isSelected = bassLine[stepIndex]?.includes(note.name); const isCurrent = currentStep === stepIndex; return (
handleBassCellMouseDown(note.name, stepIndex)} onMouseEnter={() => handleBassCellMouseEnter(note.name, stepIndex)} onDragStart={(e) => e.preventDefault()} > {isCurrent &&
} {stepIndex > 0 && stepIndex % 4 === 0 &&
}
) })}
))}
); };