fix: sửa lỗi tách bảng điều khiển TCP và timeline của track

This commit is contained in:
2026-07-18 21:17:08 +07:00
parent d14a11342a
commit f52e9b7bef
3 changed files with 383 additions and 230 deletions
+241 -230
View File
@@ -1671,32 +1671,32 @@
localDragStartTimeRef.current = time;
};
// Document-level mousemove/mouseup for local selection drag
useEffect(() => {
const handleMouseMove = (e) => {
if (!localDragInProgressRef.current) return;
const wrapper = timelineWrapperRef.current;
if (!wrapper) return;
const rect = wrapper.getBoundingClientRect();
const scrollLeft = wrapper.scrollLeft;
const mouseX = e.clientX - rect.left + scrollLeft;
const time = Math.max(0, Math.min(maxDuration, mouseX / zoom));
setLocalSelectionEnd(time);
};
const handleMouseUp = () => {
if (localDragInProgressRef.current) {
localDragInProgressRef.current = false;
localDragTrackRef.current = null;
localDragStartTimeRef.current = 0;
}
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
}, [zoom, maxDuration]);
// Document-level mousemove/mouseup for local selection drag
useEffect(() => {
const handleMouseMove = (e) => {
if (!localDragInProgressRef.current) return;
const wrapper = timelineWrapperRef.current;
if (!wrapper) return;
const rect = wrapper.getBoundingClientRect();
const scrollLeft = wrapper.scrollLeft;
const mouseX = e.clientX - rect.left + scrollLeft;
const time = Math.max(0, Math.min(maxDuration, (mouseX - 300) / zoom));
setLocalSelectionEnd(time);
};
const handleMouseUp = () => {
if (localDragInProgressRef.current) {
localDragInProgressRef.current = false;
localDragTrackRef.current = null;
localDragStartTimeRef.current = 0;
}
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
}, [zoom, maxDuration]);
const draggedClipRef = useRef(null);
draggedClipRef.current = draggedClip;
@@ -1742,7 +1742,7 @@
const rect = wrapper.getBoundingClientRect();
const scrollLeft = wrapper.scrollLeft;
const mouseX = e.clientX - rect.left + scrollLeft;
const time = mouseX / zoom;
const time = (mouseX - 300) / zoom;
const rawStart = Math.max(0, time - drag.clickOffset);
const newStart = snapTime(rawStart, snapValueRef.current, bpmRef.current);
@@ -2766,79 +2766,118 @@
)}
{/* ── Workspace ── */}
<div className="flex-1 flex overflow-y-auto select-none daw-bg relative" style={{ display: activeTab !== 'main' ? 'none' : '' }}>
{/* TCP Left Column */}
<div className="w-[300px] flex flex-col daw-panel border-r border-zinc-900 z-10 select-none shrink-0">
<div className="h-8 border-b border-zinc-900 bg-[#242424] flex items-center px-2 justify-between sticky top-0 z-30 select-none">
<span className="text-[10px] font-bold text-zinc-500 uppercase shrink-0 mr-2">Kênh</span>
{/* Timeline Toolbar */}
<div className="flex items-center gap-0.5 bg-zinc-900 border border-zinc-800 rounded px-1 py-0.5 shadow-sm">
<button
onClick={() => { setActiveTool('select'); showToast('Công cụ chọn (Select Tool) đã kích hoạt.', 'info'); }}
className={`p-1 rounded transition text-xs flex items-center justify-center ${activeTool === 'select' ? 'bg-cyan-950/80 text-cyan-400 border border-cyan-800/50 font-bold' : 'text-zinc-400 hover:bg-zinc-800 border border-transparent'}`}
title="Select Tool: Chọn khoảng, Đặt playhead (Giữ Alt kéo để di chuyển nhanh clip)"
>
<i data-lucide="mouse-pointer" className="w-3 h-3"></i>
</button>
<button
onClick={() => { setActiveTool('grab'); showToast('Công cụ di chuyển (Hand Tool) đã kích hoạt.', 'info'); }}
className={`p-1 rounded transition text-xs flex items-center justify-center ${activeTool === 'grab' ? 'bg-cyan-950/80 text-cyan-400 border border-cyan-700/50 font-bold' : 'text-zinc-400 hover:bg-zinc-800 border border-transparent'}`}
title="Grab Tool: Click kéo trực tiếp để di chuyển clip"
>
<i data-lucide="hand" className="w-3 h-3"></i>
</button>
<button
onClick={() => { setActiveTool('razor'); showToast('Công cụ chia đoạn (Razor Tool) đã kích hoạt.', 'info'); }}
className={`p-1 rounded transition text-xs flex items-center justify-center ${activeTool === 'razor' ? 'bg-cyan-950/80 text-cyan-400 border border-cyan-700/50 font-bold' : 'text-zinc-400 hover:bg-zinc-800 border border-transparent'}`}
title="Razor Tool: Click trên clip để chia nhỏ tại điểm click"
>
{/* Biểu tượng lưỡi dao cạo - dùng custom SVG hoặc scissors */}
<svg className="w-3 h-3 text-orange-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M6 3h12a2 2 0 0 1 2 2v2a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z"/>
<path d="M4 9h16l-3 9H7z"/>
<circle cx="12" cy="6" r="1"/>
</svg>
</button>
<div
ref={timelineWrapperRef}
className="flex-1 overflow-x-auto overflow-y-auto select-none daw-bg relative"
style={{ display: activeTab !== 'main' ? 'none' : '' }}
>
<div style={{ width: `calc(300px + ${timelineWidth}px)` }} className="relative flex flex-col min-h-full">
{/* [ZONE A] Header Row */}
<div className="sticky top-0 z-40 flex h-8 border-b border-zinc-900 bg-[#242424] shrink-0">
{/* Left Side: Channels & Tools Header */}
<div className="w-[300px] sticky left-0 z-50 bg-[#242424] border-r border-zinc-900 flex items-center px-2 justify-between select-none shrink-0">
<span className="text-[10px] font-bold text-zinc-500 uppercase shrink-0 mr-2">Kênh</span>
{/* Separator */}
{/* Timeline Toolbar */}
<div className="flex items-center gap-0.5 bg-zinc-900 border border-zinc-800 rounded px-1 py-0.5 shadow-sm">
<button
onClick={() => { setActiveTool('select'); showToast('Công cụ chọn (Select Tool) đã kích hoạt.', 'info'); }}
className={`p-1 rounded transition text-xs flex items-center justify-center ${activeTool === 'select' ? 'bg-cyan-950/80 text-cyan-400 border border-cyan-800/50 font-bold' : 'text-zinc-400 hover:bg-zinc-800 border border-transparent'}`}
title="Select Tool: Chọn khoảng, Đặt playhead (Giữ Alt kéo để di chuyển nhanh clip)"
>
<i data-lucide="mouse-pointer" className="w-3 h-3"></i>
</button>
<button
onClick={() => { setActiveTool('grab'); showToast('Công cụ di chuyển (Hand Tool) đã kích hoạt.', 'info'); }}
className={`p-1 rounded transition text-xs flex items-center justify-center ${activeTool === 'grab' ? 'bg-cyan-950/80 text-cyan-400 border border-cyan-700/50 font-bold' : 'text-zinc-400 hover:bg-zinc-800 border border-transparent'}`}
title="Grab Tool: Click kéo trực tiếp để di chuyển clip"
>
<i data-lucide="hand" className="w-3 h-3"></i>
</button>
<button
onClick={() => { setActiveTool('razor'); showToast('Công cụ chia đoạn (Razor Tool) đã kích hoạt.', 'info'); }}
className={`p-1 rounded transition text-xs flex items-center justify-center ${activeTool === 'razor' ? 'bg-cyan-950/80 text-cyan-400 border border-cyan-700/50 font-bold' : 'text-zinc-400 hover:bg-zinc-800 border border-transparent'}`}
title="Razor Tool: Click trên clip để chia nhỏ tại điểm click"
>
<svg className="w-3 h-3 text-orange-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M6 3h12a2 2 0 0 1 2 2v2a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z"/>
<path d="M4 9h16l-3 9H7z"/>
<circle cx="12" cy="6" r="1"/>
</svg>
</button>
{/* Separator */}
<div className="w-[1px] h-3 bg-zinc-800 mx-0.5"></div>
{/* Quick Actions (Glue, Cut, Copy, Paste) */}
<button
onClick={handleGlueTracks}
className="p-0.5 rounded text-zinc-400 hover:bg-zinc-800 hover:text-purple-400 transition"
title="Glue: Gộp track hiện tại với track liền dưới"
>
<i data-lucide="link" className="w-3 h-3"></i>
</button>
<button
onClick={handleCutTrack}
className="p-0.5 rounded text-zinc-400 hover:bg-zinc-800 hover:text-red-400 transition"
title="Cut Clip (Ctrl+X)"
>
<i data-lucide="scissors" className="w-3 h-3"></i>
</button>
<button
onClick={handleCopyTrack}
className="p-0.5 rounded text-zinc-400 hover:bg-zinc-800 hover:text-blue-400 transition"
title="Copy Clip (Ctrl+C)"
>
<i data-lucide="copy" className="w-3 h-3"></i>
</button>
<button
onClick={handlePasteTrack}
className="p-0.5 rounded text-zinc-400 hover:bg-zinc-800 hover:text-emerald-400 transition"
title="Paste Clip (Ctrl+V)"
>
<i data-lucide="clipboard" className="w-3 h-3"></i>
</button>
</div>
{/* Snap Section */}
<div className="w-[1px] h-3 bg-zinc-800 mx-0.5"></div>
{/* Quick Actions (Glue, Cut, Copy, Paste) */}
<button
onClick={handleGlueTracks}
className="p-0.5 rounded text-zinc-400 hover:bg-zinc-800 hover:text-purple-400 transition"
title="Glue: Gộp track hiện tại với track liền dưới"
>
<i data-lucide="link" className="w-3 h-3"></i>
</button>
<button
onClick={handleCutTrack}
className="p-0.5 rounded text-zinc-400 hover:bg-zinc-800 hover:text-red-400 transition"
title="Cut Clip (Ctrl+X)"
>
<i data-lucide="scissors" className="w-3 h-3"></i>
</button>
<button
onClick={handleCopyTrack}
className="p-0.5 rounded text-zinc-400 hover:bg-zinc-800 hover:text-blue-400 transition"
title="Copy Clip (Ctrl+C)"
>
<i data-lucide="copy" className="w-3 h-3"></i>
</button>
<button
onClick={handlePasteTrack}
className="p-0.5 rounded text-zinc-400 hover:bg-zinc-800 hover:text-emerald-400 transition"
title="Paste Clip (Ctrl+V)"
>
<i data-lucide="clipboard" className="w-3 h-3"></i>
</button>
<div className="flex items-center gap-1 pl-0.5 select-none">
<span className="text-[9px] text-zinc-500 font-bold uppercase">Snap</span>
<select
value={snapValue}
onChange={(e) => setSnapValue(e.target.value)}
className="bg-zinc-850 text-zinc-300 text-[10px] px-1 py-0.5 rounded border border-zinc-800 focus:outline-none focus:border-cyan-550 font-mono"
>
<option value="free">Free</option>
<option value="1">1</option>
<option value="1/2">1/2</option>
<option value="1/4">1/4</option>
<option value="1/8">1/8</option>
<option value="1/16">1/16</option>
<option value="1/32">1/32</option>
</select>
</div>
</div>
{/* Right Side: Time Ruler */}
<div ref={rulerRef} className="flex-1 relative h-full flex items-center select-none cursor-ew-resize overflow-hidden" onMouseDown={handleRulerMouseDown}>
{Array.from({ length: Math.ceil(maxDuration) }).map((_, i) => {
const sec = i;
const x = sec * zoom;
return (
<div key={i} className="absolute h-full border-l border-zinc-800 pl-1 pt-1 text-[9px] font-mono text-zinc-500 pointer-events-none" style={{ left: `${x}px` }}>
{formatTime(sec)}
</div>
);
})}
</div>
</div>
<div className="flex flex-col divide-y divide-[#141414]">
{/* Tempo Track TCP - LOOP_EDITOR_2.md §6 */}
<div className="h-[40px] p-2 flex flex-col justify-between bg-[#1a1a2e] border-l-4 border-purple-500">
{/* [ZONE A] Tempo Track Row */}
<div className="sticky top-8 z-30 flex h-[40px] border-b border-purple-500/30 bg-[#1a1a2e] shrink-0">
{/* Left Side: Tempo TCP */}
<div className="w-[300px] sticky left-0 z-50 bg-[#1a1a2e] border-r border-zinc-900 p-2 flex flex-col justify-between border-l-4 border-purple-500 shrink-0">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-[10px] font-bold text-purple-400 font-mono">TM</span>
@@ -2858,124 +2897,93 @@
</div>
</div>
</div>
{/* Right Side: Tempo Lane */}
<div className="flex-1 relative h-full overflow-hidden bg-[#1a1a2e]">
<TempoTrackLane bpm={parseInt(bpm) || 120} zoom={zoom} timelineWidth={timelineWidth}
onPlayheadSet={setCurrentTime} snapValue={snapValue} />
</div>
</div>
{/* [ZONE B] Dynamic Track List Workspace */}
<div className="flex-1 flex flex-col divide-y divide-[#141414] relative bg-[#111111] min-h-full">
{tracks.map((track, idx) => {
const isSelected = selectedTrackId === track.id;
return (
<div
key={track.id}
onClick={() => setSelectedTrackId(track.id)}
className={`h-[96px] p-2.5 flex flex-col justify-between transition-all relative cursor-pointer ${
isSelected
? 'bg-zinc-800/80 border-l-4 border-cyan-500 pl-1.5 ring-1 ring-cyan-500/30'
: 'hover:bg-zinc-800/30'
}`}
>
<div className="flex items-start justify-between">
<div className="flex items-center gap-2">
<span className="text-[10px] font-bold text-zinc-500 font-mono">{(idx+1).toString().padStart(2, '0')}</span>
<div className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: track.color }} />
<span className="text-xs font-semibold text-zinc-300 truncate max-w-[120px]" title={track.name}>
{track.name}
</span>
<div key={track.id} className={`h-[96px] flex hover:bg-zinc-850/5 transition-colors ${isSelected ? 'bg-zinc-800/10' : ''}`}>
{/* Left Column: TCP */}
<div
onClick={() => setSelectedTrackId(track.id)}
className={`w-[300px] sticky left-0 z-10 p-2.5 flex flex-col justify-between bg-[#1e1e1e] border-r border-zinc-900 shrink-0 cursor-pointer border-l-4 overflow-hidden ${isSelected ? 'border-cyan-500 bg-[#252525]' : 'border-transparent hover:bg-zinc-800/20'}`}
>
<div className="flex items-start justify-between">
<div className="flex items-center gap-2">
<span className="text-[10px] font-bold text-zinc-500 font-mono">{(idx+1).toString().padStart(2, '0')}</span>
<div className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: track.color }} />
<span className="text-xs font-semibold text-zinc-300 truncate max-w-[120px]" title={track.name}>
{track.name}
</span>
</div>
<div className="flex items-center gap-1">
<button
onClick={(e) => { e.stopPropagation(); toggleTrackMute(track.id); }}
className={`px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition ${
track.muted
? 'bg-red-950 text-red-400 border-red-700'
: 'bg-zinc-800 text-zinc-400 border-transparent hover:text-zinc-200'
}`}
>M</button>
<button
onClick={(e) => { e.stopPropagation(); toggleTrackSoloEvaluate(track.id); }}
className={`px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition ${
soloedTrackId === track.id || track.solo
? 'bg-amber-950 text-amber-400 border-amber-600'
: 'bg-zinc-800 text-zinc-400 border-transparent hover:text-zinc-200'
}`}
title="Solo nghe thử"
>S</button>
</div>
</div>
<div className="flex items-center gap-1">
<div className="flex items-center gap-1 text-[9px] text-zinc-400" onClick={e => e.stopPropagation()}>
<span className="font-semibold uppercase text-[8px] text-zinc-500"> phỏng:</span>
<button
onClick={(e) => { e.stopPropagation(); toggleTrackMute(track.id); }}
className={`px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition ${
track.muted
? 'bg-red-950 text-red-400 border-red-700'
: 'bg-zinc-800 text-zinc-400 border-transparent hover:text-zinc-200'
}`}
>M</button>
onClick={() => generateSynthToTrack(track.id, 'kick')}
className="px-1 bg-zinc-800 hover:bg-zinc-700 rounded border border-zinc-700"
>Kick Drum</button>
<button
onClick={(e) => { e.stopPropagation(); toggleTrackSoloEvaluate(track.id); }}
className={`px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition ${
soloedTrackId === track.id || track.solo
? 'bg-amber-950 text-amber-400 border-amber-600'
: 'bg-zinc-800 text-zinc-400 border-transparent hover:text-zinc-200'
}`}
title="Solo nghe thử"
>S</button>
onClick={() => generateSynthToTrack(track.id, 'synth')}
className="px-1 bg-zinc-800 hover:bg-zinc-700 rounded border border-zinc-700"
>Arpeggiator</button>
</div>
<div className="flex items-center justify-between gap-2" onClick={e => e.stopPropagation()}>
<div className="flex items-center gap-1.5">
<VolumeKnob value={track.volume} onChange={(v) => updateTrackVolume(track.id, v)} />
<span className="text-[10px] font-mono text-zinc-500">Gain: {Math.round(track.volume * 100)}%</span>
</div>
<div>
<input
type="file"
id={`upload-${track.id}`}
accept="audio/*"
className="hidden"
onChange={(e) => loadFileOnTrack(track.id, e.target.files[0])}
/>
<label
htmlFor={`upload-${track.id}`}
className="px-2 py-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded text-[10px] flex items-center gap-1 cursor-pointer transition border border-zinc-700"
>
<i data-lucide="upload" className="w-3 h-3"></i> Ti file
</label>
</div>
</div>
</div>
<div className="flex items-center gap-1 text-[9px] text-zinc-400" onClick={e => e.stopPropagation()}>
<span className="font-semibold uppercase text-[8px] text-zinc-500"> phỏng:</span>
<button
onClick={() => generateSynthToTrack(track.id, 'kick')}
className="px-1 bg-zinc-800 hover:bg-zinc-700 rounded border border-zinc-700"
>Kick Drum</button>
<button
onClick={() => generateSynthToTrack(track.id, 'synth')}
className="px-1 bg-zinc-800 hover:bg-zinc-700 rounded border border-zinc-700"
>Arpeggiator</button>
</div>
<div className="flex items-center justify-between gap-2" onClick={e => e.stopPropagation()}>
<div className="flex items-center gap-1.5">
<VolumeKnob value={track.volume} onChange={(v) => updateTrackVolume(track.id, v)} />
<span className="text-[10px] font-mono text-zinc-500">Gain: {Math.round(track.volume * 100)}%</span>
</div>
<div>
<input
type="file"
id={`upload-${track.id}`}
accept="audio/*"
className="hidden"
onChange={(e) => loadFileOnTrack(track.id, e.target.files[0])}
/>
<label
htmlFor={`upload-${track.id}`}
className="px-2 py-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded text-[10px] flex items-center gap-1 cursor-pointer transition border border-zinc-700"
>
<i data-lucide="upload" className="w-3 h-3"></i> Ti file
</label>
</div>
</div>
</div>
);
})}
</div>
</div>
{/* Timeline Right Canvas */}
<div
ref={timelineWrapperRef}
className="flex-1 overflow-x-auto relative bg-[#111111]"
>
<div style={{ width: `${timelineWidth}px` }} className="relative flex flex-col h-full">
{/* Ruler - LOOP_MAKER.md VÙNG A: Global Selection hitbox */}
<div ref={rulerRef}
className="h-8 border-b border-zinc-900 bg-[#242424] sticky top-0 z-30 flex items-center select-none shrink-0 cursor-ew-resize"
onMouseDown={handleRulerMouseDown}
>
{Array.from({ length: Math.ceil(maxDuration) }).map((_, i) => {
const sec = i;
const x = sec * zoom;
return (
<div key={i} className="absolute h-full border-l border-zinc-800 pl-1 pt-1 text-[9px] font-mono text-zinc-500 pointer-events-none" style={{ left: `${x}px` }}>
{formatTime(sec)}
</div>
);
})}
</div>
{/* Stacked Lanes */}
<div className="flex-1 flex flex-col relative divide-y divide-[#141414]">
{/* Tempo Track Lane - LOOP_EDITOR_2.md §6 */}
<div className="h-[40px] w-full relative flex-shrink-0 border-b border-purple-500/30">
<TempoTrackLane bpm={parseInt(bpm) || 120} zoom={zoom} timelineWidth={timelineWidth}
onPlayheadSet={setCurrentTime} snapValue={snapValue} />
</div>
{tracks.map((track) => {
const isSelected = selectedTrackId === track.id;
return (
<div key={track.id} className="h-[96px] w-full relative flex-shrink-0"
{/* Right Column: Waveform Lane */}
<div
className="flex-1 relative overflow-hidden h-full"
onDragOver={(e) => e.preventDefault()}
onDrop={(e) => {
e.preventDefault();
@@ -3003,19 +3011,22 @@
{track.buffer && (
<div className="absolute right-2 top-2 flex items-center gap-1 z-10 opacity-70 hover:opacity-100 transition">
<button onClick={() => handleSplitTrack(track.id)}
className="px-1.5 py-0.5 bg-zinc-900/90 text-zinc-300 rounded text-[9px] flex items-center gap-1 border border-zinc-700/50"
className="px-1.5 py-0.5 bg-zinc-900/95 text-zinc-300 rounded text-[9px] flex items-center gap-1 border border-zinc-700/50"
title="Cắt đoạn tại Playhead">
<i data-lucide="scissors" className="w-2.5 h-2.5 text-cyan-400"></i> Ct
</button>
</div>
)}
</div>
);
})}
</div>
);
})}
{/* Drop zone to create a new track during drag */}
{/* Bottom Drop Zone to create new track */}
<div className="h-[48px] flex border-t border-dashed border-zinc-800">
<div className="w-[300px] sticky left-0 z-10 bg-[#1e1e1e]/50 border-r border-zinc-900 shrink-0"></div>
<div
className="h-[48px] w-full border-t border-dashed border-zinc-800 flex items-center justify-center text-xs text-zinc-500 hover:bg-zinc-900/20 cursor-pointer select-none shrink-0"
className="flex-1 flex items-center justify-center text-xs text-zinc-500 hover:bg-zinc-900/20 cursor-pointer select-none"
onMouseEnter={() => {
if (draggedClipRef.current) {
const newId = addNewTrack();
@@ -3028,32 +3039,32 @@
<i data-lucide="plus" className="w-3.5 h-3.5"></i> Kéo clip xung đây hoc Click đ to Track mi
</span>
</div>
</div>
{/* Selection Overlay - only for Global mode (LOOP_EDITOR.md §1.2: local draws on canvas per-track) */}
{selectionMode !== 'local' && selLeft !== null && selRight !== null && selRight > selLeft && (
<div className="absolute top-8 bottom-0 border-l border-r border-amber-500 bg-amber-500/10 z-20 selection-interactive-box cursor-grab active:cursor-grabbing"
style={{ left: `${selLeft * zoom}px`, width: `${(selRight - selLeft) * zoom}px` }}
onMouseDown={handleSelectionBodyDragStart}
title="Kéo để di chuyển vùng chọn"
>
<div className="absolute -left-1.5 top-0 bottom-0 w-3 bg-amber-500 hover:bg-amber-400 cursor-ew-resize flex items-center justify-center z-30 transition-colors"
onMouseDown={(e) => handleHandleDragStart(e, 'left')}
title="Kéo giãn mốc bắt đầu">
<div className="w-[1.5px] h-4 bg-zinc-950/70 rounded"></div>
</div>
<div className="absolute -right-1.5 top-0 bottom-0 w-3 bg-amber-500 hover:bg-amber-400 cursor-ew-resize flex items-center justify-center z-30 transition-colors"
onMouseDown={(e) => handleHandleDragStart(e, 'right')}
title="Kéo giãn mốc kết thúc">
<div className="w-[1.5px] h-4 bg-zinc-950/70 rounded"></div>
</div>
{/* Selection Overlay - only for Global mode (LOOP_EDITOR.md §1.2: local draws on canvas per-track) */}
{selectionMode !== 'local' && selLeft !== null && selRight !== null && selRight > selLeft && (
<div className="absolute top-0 bottom-0 border-l border-r border-amber-500 bg-amber-500/10 z-20 selection-interactive-box cursor-grab active:cursor-grabbing"
style={{ left: `${selLeft * zoom + 300}px`, width: `${(selRight - selLeft) * zoom}px` }}
onMouseDown={handleSelectionBodyDragStart}
title="Kéo để di chuyển vùng chọn"
>
<div className="absolute -left-1.5 top-0 bottom-0 w-3 bg-amber-500 hover:bg-amber-400 cursor-ew-resize flex items-center justify-center z-30 transition-colors"
onMouseDown={(e) => handleHandleDragStart(e, 'left')}
title="Kéo giãn mốc bắt đầu">
<div className="w-[1.5px] h-4 bg-zinc-950/70 rounded"></div>
</div>
<div className="absolute -right-1.5 top-0 bottom-0 w-3 bg-amber-500 hover:bg-amber-400 cursor-ew-resize flex items-center justify-center z-30 transition-colors"
onMouseDown={(e) => handleHandleDragStart(e, 'right')}
title="Kéo giãn mốc kết thúc">
<div className="w-[1.5px] h-4 bg-zinc-950/70 rounded"></div>
</div>
)}
{/* Playhead */}
<div className="absolute top-0 bottom-0 w-[2px] bg-[#ef4444] z-20 pointer-events-none"
style={{ left: `${playheadLeftPos}px` }}>
<div className="w-3 h-3 bg-[#ef4444] rotate-45 transform -translate-x-1/2 -translate-y-1/2 absolute top-0"></div>
</div>
)}
{/* Playhead */}
<div className="absolute top-0 bottom-0 w-[2px] bg-[#ef4444] z-20 pointer-events-none"
style={{ left: `${playheadLeftPos + 300}px` }}>
<div className="w-3 h-3 bg-[#ef4444] rotate-45 transform -translate-x-1/2 -translate-y-1/2 absolute top-0"></div>
</div>
</div>
</div>