From 1dc0604635ddea29b516b17badfb6090edb18d59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Tue, 18 Nov 2025 14:25:49 +0100 Subject: [PATCH] feat: add intuitive drag-to-select on waveforms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Improved selection UX to match professional DAWs: - Drag directly on waveform to create selections (no modifier keys needed) - Click without dragging seeks playhead and clears selection - 3-pixel drag threshold prevents accidental selections - Fixed variable name conflict with existing file drag-and-drop feature Users can now naturally drag across waveforms to select regions for editing, providing a more intuitive workflow. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- components/tracks/Track.tsx | 69 ++++++++++++++++++++++++++----------- 1 file changed, 49 insertions(+), 20 deletions(-) diff --git a/components/tracks/Track.tsx b/components/tracks/Track.tsx index 5fee5bd..fcd4daf 100644 --- a/components/tracks/Track.tsx +++ b/components/tracks/Track.tsx @@ -68,6 +68,8 @@ export function Track({ // Selection state const [isSelecting, setIsSelecting] = React.useState(false); const [selectionStart, setSelectionStart] = React.useState(null); + const [isSelectingByDrag, setIsSelectingByDrag] = React.useState(false); + const [dragStartPos, setDragStartPos] = React.useState<{ x: number; y: number } | null>(null); const handleNameClick = () => { setIsEditingName(true); @@ -224,41 +226,66 @@ export function Track({ const rect = e.currentTarget.getBoundingClientRect(); const x = e.clientX - rect.left; + const y = e.clientY - rect.top; const clickTime = (x / rect.width) * duration; - // Shift+drag to create selection - if (e.shiftKey) { - setIsSelecting(true); - setSelectionStart(clickTime); - onSelectionChange?.({ start: clickTime, end: clickTime }); - } else { - // Regular click clears selection and seeks - onSelectionChange?.(null); - if (onSeek) { - onSeek(clickTime); - } - } + // Store drag start position + setDragStartPos({ x: e.clientX, y: e.clientY }); + setIsSelectingByDrag(false); + + // Start selection immediately (will be used if user drags) + setIsSelecting(true); + setSelectionStart(clickTime); }; const handleCanvasMouseMove = (e: React.MouseEvent) => { - if (!isSelecting || selectionStart === null || !duration) return; + if (!isSelecting || selectionStart === null || !duration || !dragStartPos) return; const rect = e.currentTarget.getBoundingClientRect(); const x = e.clientX - rect.left; const currentTime = (x / rect.width) * duration; - // Clamp to valid time range - const clampedTime = Math.max(0, Math.min(duration, currentTime)); + // Check if user has moved enough to be considered dragging (threshold: 3 pixels) + const dragDistance = Math.sqrt( + Math.pow(e.clientX - dragStartPos.x, 2) + Math.pow(e.clientY - dragStartPos.y, 2) + ); - // Update selection (ensure start < end) - const start = Math.min(selectionStart, clampedTime); - const end = Math.max(selectionStart, clampedTime); + if (dragDistance > 3) { + setIsSelectingByDrag(true); + } - onSelectionChange?.({ start, end }); + // If dragging, update selection + if (isSelectingByDrag || dragDistance > 3) { + // Clamp to valid time range + const clampedTime = Math.max(0, Math.min(duration, currentTime)); + + // Update selection (ensure start < end) + const start = Math.min(selectionStart, clampedTime); + const end = Math.max(selectionStart, clampedTime); + + onSelectionChange?.({ start, end }); + } }; - const handleCanvasMouseUp = () => { + const handleCanvasMouseUp = (e: React.MouseEvent) => { + if (!duration) return; + + const rect = e.currentTarget.getBoundingClientRect(); + const x = e.clientX - rect.left; + const clickTime = (x / rect.width) * duration; + + // If user didn't drag (just clicked), clear selection and seek + if (!isSelectingByDrag) { + onSelectionChange?.(null); + if (onSeek) { + onSeek(clickTime); + } + } + + // Reset drag state setIsSelecting(false); + setIsSelectingByDrag(false); + setDragStartPos(null); }; // Handle mouse leaving canvas during selection @@ -266,6 +293,8 @@ export function Track({ const handleGlobalMouseUp = () => { if (isSelecting) { setIsSelecting(false); + setIsSelectingByDrag(false); + setDragStartPos(null); } };