11import type { ReactNode } from "react" ;
22
33import { Box , ButtonBase , Typography } from "@mui/material" ;
4+ import { useRef , useEffect } from "react" ;
45
56import WavesIcon from "@mui/icons-material/ShowChart" ;
67import MediaIcon from "@mui/icons-material/AudioFile" ;
@@ -69,8 +70,63 @@ type ControlsProps = {
6970} ;
7071
7172export function Controls ( { section, setSection } : ControlsProps ) {
73+ // Small passive touch-listener detects horizontal drags so clicks are suppressed
74+ // while a user is scrolling the bar. This keeps native scrolling behavior and
75+ // avoids reimplementing drag-to-scroll.
76+ const containerRef = useRef < HTMLDivElement | null > ( null ) ;
77+ const isDraggingRef = useRef ( false ) ;
78+
79+ useEffect ( ( ) => {
80+ const el = containerRef . current ;
81+ if ( ! el ) return ;
82+
83+ let startX = 0 ;
84+ const THRESHOLD = 6 ; // pixels
85+
86+ const onTouchStart = ( e : TouchEvent ) => {
87+ if ( e . touches . length !== 1 ) return ;
88+ startX = e . touches [ 0 ] . clientX ;
89+ isDraggingRef . current = false ;
90+ } ;
91+
92+ const onTouchMove = ( e : TouchEvent ) => {
93+ if ( e . touches . length !== 1 ) return ;
94+ const dx = e . touches [ 0 ] . clientX - startX ;
95+ if ( Math . abs ( dx ) > THRESHOLD ) {
96+ isDraggingRef . current = true ;
97+ }
98+ } ;
99+
100+ const onTouchEnd = ( ) => {
101+ // small delay so immediate click after lift doesn't fire
102+ setTimeout ( ( ) => {
103+ isDraggingRef . current = false ;
104+ } , 50 ) ;
105+ } ;
106+
107+ el . addEventListener ( "touchstart" , onTouchStart , { passive : true } ) ;
108+ el . addEventListener ( "touchmove" , onTouchMove , { passive : true } ) ;
109+ el . addEventListener ( "touchend" , onTouchEnd , { passive : true } ) ;
110+
111+ return ( ) => {
112+ el . removeEventListener ( "touchstart" , onTouchStart as EventListener ) ;
113+ el . removeEventListener ( "touchmove" , onTouchMove as EventListener ) ;
114+ el . removeEventListener ( "touchend" , onTouchEnd as EventListener ) ;
115+ } ;
116+ } , [ ] ) ;
117+
118+ const onItemClick = ( key : ControlKey , e : React . MouseEvent ) => {
119+ if ( isDraggingRef . current ) {
120+ e . preventDefault ( ) ;
121+ e . stopPropagation ( ) ;
122+ return ;
123+ }
124+ setSection ( key ) ;
125+ } ;
126+
72127 return (
73128 < Box
129+ ref = { containerRef }
74130 sx = { {
75131 display : "flex" ,
76132 flexDirection : { xs : "row" , md : "column" } ,
@@ -82,12 +138,17 @@ export function Controls({ section, setSection }: ControlsProps) {
82138 height : "100%" ,
83139 // enable smooth momentum scrolling on iOS
84140 WebkitOverflowScrolling : "touch" ,
85- // prefer horizontal pan on touch devices
141+ // let the browser handle horizontal panning natively
86142 touchAction : { xs : "pan-x" , md : "auto" } ,
87143 // enable scroll snap on mobile so items align nicely and scrolling feels natural
88144 scrollSnapType : { xs : "x mandatory" , md : "none" } ,
89145 // give a bit of horizontal padding so items aren't flush to the edge
90146 px : { xs : 1 , md : 0 } ,
147+ // prevent text selection while dragging
148+ userSelect : { xs : "none" , md : "auto" } ,
149+ WebkitUserSelect : { xs : "none" , md : "auto" } ,
150+ // keep overscroll within the container to avoid parent scrolling
151+ overscrollBehavior : { xs : "contain" , md : "auto" } ,
91152 // hide the default webkit scrollbar for cleaner look on mobile
92153 "&::-webkit-scrollbar" : { display : { xs : "none" , md : "block" } } ,
93154 } }
@@ -98,7 +159,7 @@ export function Controls({ section, setSection }: ControlsProps) {
98159 return (
99160 < ButtonBase
100161 key = { item . key }
101- onClick = { ( ) => setSection ( item . key ) }
162+ onClick = { ( e ) => onItemClick ( item . key , e ) }
102163 aria-pressed = { isActive }
103164 sx = { {
104165 display : "flex" ,
@@ -116,6 +177,8 @@ export function Controls({ section, setSection }: ControlsProps) {
116177 flexShrink : { xs : 0 , md : 1 } ,
117178 scrollSnapAlign : { xs : "center" , md : "none" } ,
118179 minWidth : { xs : 88 , md : "auto" } ,
180+ // let touches on buttons still allow native pan gestures
181+ touchAction : { xs : "manipulation" , md : "auto" } ,
119182 bgcolor : isActive
120183 ? { xs : "transparent" , md : "action.selected" }
121184 : undefined ,
0 commit comments