feat: add intuitive drag-to-select on waveforms

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 <noreply@anthropic.com>
This commit is contained in:
2025-11-18 14:25:49 +01:00
parent 74879a42cf
commit 1dc0604635

View File

@@ -68,6 +68,8 @@ export function Track({
// Selection state
const [isSelecting, setIsSelecting] = React.useState(false);
const [selectionStart, setSelectionStart] = React.useState<number | null>(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<HTMLCanvasElement>) => {
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<HTMLCanvasElement>) => {
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);
}
};