@@ -78,17 +78,20 @@ export const VideoTimeline = ({
7878 return Math . max ( 0 , Math . min ( duration , ( relativeX / width ) * duration ) ) ;
7979 } ;
8080
81+ const [ isScrubbing , setIsScrubbing ] = useState ( false ) ;
82+
8183 const handleTimelineMouseDown = ( e : React . MouseEvent ) => {
82- if ( trimMode ) return ; // Don't pan in trim mode to avoid conflict with handles
83- if ( e . button === 1 || ( e . button === 0 && e . altKey ) ) { // Middle click or Alt+Left click to pan
84+ if ( trimMode ) return ;
85+ if ( e . button === 1 || ( e . button === 0 && e . altKey ) ) {
8486 e . preventDefault ( ) ;
8587 setIsPanning ( true ) ;
8688 setPanStartX ( e . clientX ) ;
8789 if ( containerRef . current ) {
8890 setPanStartScroll ( containerRef . current . scrollLeft ) ;
8991 }
9092 } else if ( e . button === 0 ) {
91- // Normal click to seek
93+ // Scrubbing Start
94+ setIsScrubbing ( true ) ;
9295 const time = xToTime ( e . clientX ) ;
9396 onSeek ( time ) ;
9497 }
@@ -103,17 +106,48 @@ export const VideoTimeline = ({
103106 containerRef . current . scrollLeft = panStartScroll - delta ;
104107 setScrollLeft ( containerRef . current . scrollLeft ) ;
105108 }
109+
110+ if ( isScrubbing ) {
111+ onSeek ( time ) ;
112+ }
106113 } ;
107114
108115 const handleMouseLeave = ( ) => {
109116 setHoveredTime ( null ) ;
110117 setIsPanning ( false ) ;
118+ // Don't stop scrubbing on leave, let global mouse up handle it?
119+ // Or if we leave the track, maybe we should stop scrubbing if we didn't capture pointer?
120+ // Since we are using simple div events, let's keep it simple: Stop scrubbing on leave or global up.
121+ // Better UX: add global listener for scrubbing.
111122 } ;
112123
113124 const handleMouseUp = ( ) => {
114125 setIsPanning ( false ) ;
126+ setIsScrubbing ( false ) ;
115127 } ;
116128
129+ // Global Mouse Up to catching dragging outside
130+ useEffect ( ( ) => {
131+ if ( isScrubbing ) {
132+ const handleGlobalUp = ( ) => setIsScrubbing ( false ) ;
133+ const handleGlobalMove = ( e : MouseEvent ) => {
134+ // Calculate time relative to container
135+ if ( containerRef . current ) {
136+ // Logic duplicated from xToTime but needs clientX
137+ // We can reuse xToTime if we pass clientX
138+ const time = xToTime ( e . clientX ) ;
139+ onSeek ( time ) ;
140+ }
141+ } ;
142+ window . addEventListener ( 'mouseup' , handleGlobalUp ) ;
143+ window . addEventListener ( 'mousemove' , handleGlobalMove ) ;
144+ return ( ) => {
145+ window . removeEventListener ( 'mouseup' , handleGlobalUp ) ;
146+ window . removeEventListener ( 'mousemove' , handleGlobalMove ) ;
147+ } ;
148+ }
149+ } , [ isScrubbing , onSeek , duration , zoomLevel , scrollLeft ] ) ; // Re-attach if deps change
150+
117151 // Handle Zoom (Wheel)
118152 const handleWheel = ( e : React . WheelEvent ) => {
119153 if ( e . ctrlKey || e . metaKey ) {
@@ -447,12 +481,14 @@ export const VideoTimeline = ({
447481 </ >
448482 ) }
449483
450- { /* Playhead */ }
484+ { /* Playhead - Interactive & Larger Target */ }
451485 < div
452- className = "absolute top-0 bottom-0 w-0.5 bg-red-500 z-30"
486+ className = "absolute top-0 bottom-0 w-0.5 bg-red-500 z-50 pointer-events-none" // pointer-events-none lets clicks pass to track, but we handle scrubbing globally
453487 style = { { left : `${ ( playbackTime / duration ) * 100 } %` } }
454488 >
455- < div className = "absolute -top-1 -left-1.5 w-3 h-3 bg-red-500 rounded-full sticky left-0" />
489+ { /* Visual Handle */ }
490+ < div className = "absolute -top-1.5 -left-2 w-4 h-4 bg-red-500 rounded-full shadow-md border-2 border-white ring-2 ring-red-500/30" />
491+ { /* Hit Area for grabbing (if we wanted specific handle grab, but track grab is easier) */ }
456492 </ div >
457493
458494 { /* Hover Indicator */ }
0 commit comments