@@ -141,17 +141,31 @@ export const VideoTimeline = ({
141141 const handleGlobalMove = ( e : MouseEvent ) => {
142142 // Calculate time relative to container
143143 if ( containerRef . current ) {
144- // Logic duplicated from xToTime but needs clientX
145- // We can reuse xToTime if we pass clientX
146144 const time = xToTime ( e . clientX ) ;
147- onSeek ( time ) ;
145+ if ( onSeek ) onSeek ( time ) ;
148146 }
149147 } ;
148+ const handleGlobalTouchMove = ( e : TouchEvent ) => {
149+ if ( containerRef . current && e . touches . length > 0 ) {
150+ const time = xToTime ( e . touches [ 0 ] . clientX ) ;
151+ if ( onSeek ) onSeek ( time ) ;
152+ }
153+ } ;
154+
150155 window . addEventListener ( 'mouseup' , handleGlobalUp ) ;
151156 window . addEventListener ( 'mousemove' , handleGlobalMove ) ;
157+ window . addEventListener ( 'touchend' , handleGlobalUp ) ;
158+ window . addEventListener ( 'touchcancel' , handleGlobalUp ) ;
159+ // using passive: false so we could potentially prevent scroll if we wanted to lock it to horizontal only,
160+ // but typical timeline behavior on mobile is okay with default touch mapping unless it scrolls
161+ window . addEventListener ( 'touchmove' , handleGlobalTouchMove , { passive : true } ) ;
162+
152163 return ( ) => {
153164 window . removeEventListener ( 'mouseup' , handleGlobalUp ) ;
154165 window . removeEventListener ( 'mousemove' , handleGlobalMove ) ;
166+ window . removeEventListener ( 'touchend' , handleGlobalUp ) ;
167+ window . removeEventListener ( 'touchcancel' , handleGlobalUp ) ;
168+ window . removeEventListener ( 'touchmove' , handleGlobalTouchMove ) ;
155169 } ;
156170 }
157171 } , [ isScrubbing , onSeek , duration , zoomLevel , scrollLeft ] ) ; // Re-attach if deps change
@@ -636,8 +650,24 @@ export const VideoTimeline = ({
636650 className = "absolute top-0 bottom-0 w-0.5 bg-red-500 z-50 pointer-events-none"
637651 style = { { left : `${ ( playbackTime / duration ) * 100 } %` } }
638652 >
639- < div className = "absolute top-0 -translate-y-1/2 left-1/2 -translate-x-1/2 w-5 h-5 md:w-3 md:h-3 bg-red-500 rounded-full border-2 md:border border-white shadow-md flex items-center justify-center" >
640- < div className = "w-1.5 h-1.5 bg-white/90 rounded-full md:hidden" />
653+ { /* Interactive Safe Area Wrapper for Mobile Grabbing */ }
654+ < div
655+ className = "absolute top-0 -translate-y-1/2 left-1/2 -translate-x-1/2 w-12 h-12 md:w-6 md:h-6 flex items-center justify-center cursor-ew-resize pointer-events-auto"
656+ onMouseDown = { ( e ) => {
657+ e . stopPropagation ( ) ;
658+ e . preventDefault ( ) ;
659+ setIsScrubbing ( true ) ;
660+ if ( onSeek ) onSeek ( playbackTime ) ;
661+ } }
662+ onTouchStart = { ( e ) => {
663+ e . stopPropagation ( ) ;
664+ setIsScrubbing ( true ) ;
665+ } }
666+ >
667+ { /* Visual Red Dot */ }
668+ < div className = "w-5 h-5 md:w-3 md:h-3 bg-red-500 rounded-full border-2 md:border border-white shadow-md flex items-center justify-center" >
669+ < div className = "w-1.5 h-1.5 bg-white/90 rounded-full md:hidden" />
670+ </ div >
641671 </ div >
642672 </ div >
643673
0 commit comments