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 } 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 ControlButton: React.FC<{ onClick?: () => void; children: React.ReactNode; className?: string; disabled?: boolean; title?: string }> = ({ onClick, children, className, disabled, title }) => ( ); const IconButton: 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, 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 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 incrementSteps = () => { const newSteps = steps + 4; if (newSteps <= MAX_STEPS) handleStepsChange(newSteps); }; const decrementSteps = () => { const newSteps = steps - 4; if (newSteps >= MIN_STEPS) handleStepsChange(newSteps); }; return (
setIsClearModalOpen(false)} onConfirm={() => { clearPattern(); setIsClearModalOpen(false); }} title="Clear Session" >

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

{/* Toolbar */}
{isPlaying ? <> Stop : <> Play} setIsClearModalOpen(true)}> Clear
Tempo
{tempo} BPM = MAX_TEMPO}>
Steps
{steps} = MAX_STEPS}>
fileInputRef.current?.click()}> Import Export
{/* Drum Machine Section */}
handleDrumVolumeChange(Number(e.target.value))} className="absolute inset-0 opacity-0 cursor-pointer [writing-mode:vertical-lr] [direction:rtl]" />
Drums
{/* Header helper */}
{Array.from({ length: steps }, (_, i) => (
{i % 4 === 0 ? (i / 4 + 1) : ''}
))} {INSTRUMENTS.map((instrument, instIndex) => (
{instrument.name}
{Array.from({ length: steps }).map((_, stepIndex) => { const isActive = grid[instIndex]?.[stepIndex]; const isCurrent = currentStep === stepIndex; const isFourth = stepIndex % 4 === 0; return (
handleCellMouseDown(instIndex, stepIndex)} onMouseEnter={() => handleCellMouseEnter(instIndex, stepIndex)} className={`h-9 rounded-sm cursor-pointer transition-all duration-75 relative group overflow-hidden ${isActive ? 'bg-orange-500 shadow-md shadow-orange-200' : 'bg-slate-100 hover:bg-slate-200'} ${isCurrent ? 'ring-2 ring-blue-400 ring-offset-1 z-10' : ''} ${isFourth && stepIndex !== 0 ? 'border-l-2 border-slate-200/50' : ''} `} > {isCurrent &&
}
) })}
))}
{/* Bass Section */}
handleBassVolumeChange(Number(e.target.value))} className="absolute inset-0 opacity-0 cursor-pointer [writing-mode:vertical-lr] [direction:rtl]" />
Bass
{/* Header helper */}
{Array.from({ length: steps }, (_, i) => (
{i % 4 === 0 ? (i / 4 + 1) : ''}
))} {[...BASS_NOTES].reverse().map((note) => (
{note.name}
{Array.from({ length: steps }).map((_, stepIndex) => { const isSelected = bassLine[stepIndex]?.includes(note.name); const isCurrent = currentStep === stepIndex; const isFourth = stepIndex % 4 === 0; return (
handleBassCellMouseDown(note.name, stepIndex)} onMouseEnter={() => handleBassCellMouseEnter(note.name, stepIndex)} className={`h-5 border-b border-r border-slate-50 cursor-pointer transition-all duration-75 relative ${isSelected ? 'bg-purple-500 shadow-inner' : note.isSharp ? 'bg-slate-50/50 hover:bg-slate-100' : 'bg-white hover:bg-slate-50'} ${isCurrent ? 'z-10 bg-blue-400/20' : ''} ${isFourth && stepIndex !== 0 ? 'border-l-2 border-slate-100' : ''} `} > {isCurrent &&
}
) })}
))}
); };