@@ -21,7 +21,7 @@ export default function TrackClipCanvas({ track, zoomLevel = 100, height = 100,
2121 } = useMultitrack ( ) ;
2222
2323 const canvasRef = useRef ( null ) ;
24- const dragRef = useRef ( { op : null , clipIndex : - 1 , startX : 0 , pxPerSecCSS : 1 , orig : null } ) ;
24+ const dragRef = useRef ( { op : null , clipIndex : - 1 , startX : 0 , pxPerSecCSS : 1 , orig : null , sourceDuration : null } ) ;
2525 // selectionBoxRef removed - selection box now handled by SelectionOverlay component
2626 const [ peaksCache , setPeaksCache ] = useState ( new Map ( ) ) ; // clip.id -> peaks
2727 const clips = Array . isArray ( track ?. clips ) ? track . clips : [ ] ;
@@ -429,15 +429,12 @@ export default function TrackClipCanvas({ track, zoomLevel = 100, height = 100,
429429
430430 // Handle select tool
431431 if ( editorTool === 'select' ) {
432- console . log ( '🔶 TrackClipCanvas: Select tool click' , { trackId : track . id , hitIndex : hit . index , clipCount : clips . length } ) ;
433432 if ( hit . index >= 0 ) {
434433 // Clicked on a clip - handle selection
435434 const c = clips [ hit . index ] ;
436435 const isShift = e . shiftKey ;
437436 const isCtrl = e . ctrlKey || e . metaKey ;
438437
439- console . log ( '🔶 TrackClipCanvas: Clicked on clip' , { clipId : c . id , isShift, isCtrl } ) ;
440-
441438 if ( isShift || isCtrl ) {
442439 // Add to or toggle from selection
443440 if ( selectedClipIds . includes ( c . id ) ) {
@@ -449,18 +446,22 @@ export default function TrackClipCanvas({ track, zoomLevel = 100, height = 100,
449446 // Single select (replace selection)
450447 setSelectedClipId ( c . id ) ;
451448 setSelectedClipIds ( [ c . id ] ) ;
452- // Set track as selected when doing single selection
453449 setSelectedTrackId ( track . id ) ;
454450 }
455451
452+ // Initialize drag so the clip can be moved in one click-drag motion
453+ const op = hit . edge === 'L' ? 'resizeL' : hit . edge === 'R' ? 'resizeR' : 'move' ;
454+ dragRef . current . op = op ;
455+ dragRef . current . clipIndex = hit . index ;
456+ dragRef . current . startX = e . clientX ;
457+ dragRef . current . orig = { start : c . start || 0 , duration : c . duration || 0 , offset : c . offset || 0 } ;
458+ dragRef . current . sourceDuration = c . sourceDuration || null ;
459+
456460 // Stop propagation so SelectionOverlay doesn't interfere
457461 e . stopPropagation ( ) ;
458462 return ;
459- } else {
460- console . log ( '🔶 TrackClipCanvas: Clicked on empty space, letting SelectionOverlay handle it' ) ;
461463 }
462- // Note: Selection box dragging is now handled by SelectionOverlay component
463- // which operates at the container level for cross-track selection
464+ // Empty space click — let SelectionOverlay handle it
464465 return ;
465466 }
466467
@@ -514,6 +515,7 @@ export default function TrackClipCanvas({ track, zoomLevel = 100, height = 100,
514515 dragRef . current . clipIndex = hit . index ;
515516 dragRef . current . startX = e . clientX ;
516517 dragRef . current . orig = { start : c . start || 0 , duration : c . duration || 0 , offset : c . offset || 0 } ;
518+ dragRef . current . sourceDuration = c . sourceDuration || null ;
517519 } else {
518520 dragRef . current . op = null ;
519521 dragRef . current . clipIndex = - 1 ;
@@ -546,6 +548,7 @@ export default function TrackClipCanvas({ track, zoomLevel = 100, height = 100,
546548 const dxSecRaw = dxCss / dragRef . current . pxPerSecCSS ;
547549 const dxSec = snapEnabled ? quantize ( dxSecRaw ) : dxSecRaw ;
548550 const { start, duration : dur , offset } = dragRef . current . orig ;
551+ const srcDur = dragRef . current . sourceDuration ; // total audio buffer length
549552 const op = dragRef . current . op ;
550553 let newStart = start ;
551554 let newDur = dur ;
@@ -554,12 +557,25 @@ export default function TrackClipCanvas({ track, zoomLevel = 100, height = 100,
554557 if ( op === 'move' ) {
555558 newStart = Math . max ( 0 , start + dxSec ) ;
556559 } else if ( op === 'resizeL' ) {
560+ // Trim from left: advance offset into the buffer, can't go past offset 0
557561 newStart = Math . max ( 0 , start + dxSec ) ;
558562 const delta = newStart - start ;
559- newDur = Math . max ( MIN_DUR , dur - delta ) ;
560563 newOffset = Math . max ( 0 , ( offset || 0 ) + delta ) ;
564+ newDur = Math . max ( MIN_DUR , dur - delta ) ;
565+ // Clamp: can't reveal audio before the buffer start
566+ if ( newOffset < 0 ) {
567+ const correction = - newOffset ;
568+ newOffset = 0 ;
569+ newStart += correction ;
570+ newDur -= correction ;
571+ }
561572 } else if ( op === 'resizeR' ) {
562573 newDur = Math . max ( MIN_DUR , dur + dxSec ) ;
574+ // Clamp: can't extend past the end of the source audio buffer
575+ if ( srcDur != null ) {
576+ const maxDur = srcDur - ( newOffset || offset || 0 ) ;
577+ newDur = Math . min ( newDur , maxDur ) ;
578+ }
563579 }
564580
565581 draw ( ) ;
0 commit comments