77 useMotionValueEvent ,
88 useTransform ,
99} from 'motion/react' ;
10- import { useEffect , useRef , useState } from 'react' ;
10+ import { useCallback , useRef , useState } from 'react' ;
1111import { cn } from '@/lib/utils' ;
1212
1313const MAX_OVERFLOW = 30 ;
@@ -25,6 +25,13 @@ interface SliderProps {
2525 disabled ?: boolean ;
2626}
2727
28+ function decay ( value : number , maxValue : number ) : number {
29+ if ( maxValue === 0 ) return 0 ;
30+ const entry = value / maxValue ;
31+ const sigmoid = 2 * ( 1 / ( 1 + Math . exp ( - entry ) ) - 0.5 ) ;
32+ return sigmoid * maxValue ;
33+ }
34+
2835export function Slider ( {
2936 value = 0 ,
3037 onValueChange,
@@ -45,84 +52,75 @@ export function Slider({
4552 const clientX = useMotionValue ( 0 ) ;
4653 const overflow = useMotionValue ( 0 ) ;
4754
48- useEffect ( ( ) => {
49- setInternalValue ( value ) ;
50- } , [ value ] ) ;
55+ const percentage = ( ( internalValue - min ) / ( max - min || 1 ) ) * 100 ;
5156
5257 useMotionValueEvent ( clientX , 'change' , ( latest : number ) => {
53- if ( sliderRef . current && isDragging ) {
54- const { left, right } = sliderRef . current . getBoundingClientRect ( ) ;
55- let newOverflow : number ;
56-
57- if ( latest < left ) {
58- setRegion ( 'left' ) ;
59- newOverflow = left - latest ;
60- } else if ( latest > right ) {
61- setRegion ( 'right' ) ;
62- newOverflow = latest - right ;
63- } else {
64- setRegion ( 'middle' ) ;
65- newOverflow = 0 ;
66- }
67-
68- overflow . jump ( decay ( newOverflow , MAX_OVERFLOW ) ) ;
58+ if ( ! sliderRef . current || ! isDragging ) return ;
59+
60+ const { left, right } = sliderRef . current . getBoundingClientRect ( ) ;
61+ let newOverflow = 0 ;
62+
63+ if ( latest < left ) {
64+ setRegion ( 'left' ) ;
65+ newOverflow = left - latest ;
66+ } else if ( latest > right ) {
67+ setRegion ( 'right' ) ;
68+ newOverflow = latest - right ;
69+ } else {
70+ setRegion ( 'middle' ) ;
6971 }
70- } ) ;
7172
72- const handlePointerMove = ( e : React . PointerEvent < HTMLDivElement > ) => {
73- if ( ! isDragging || disabled || ! sliderRef . current ) {
74- return ;
75- }
73+ overflow . jump ( decay ( newOverflow , MAX_OVERFLOW ) ) ;
74+ } ) ;
7675
77- const { left, width } = sliderRef . current . getBoundingClientRect ( ) ;
78- let newValue = min + ( ( e . clientX - left ) / width ) * ( max - min ) ;
76+ const updateValue = useCallback (
77+ ( clientXPos : number ) => {
78+ if ( ! sliderRef . current ) return ;
7979
80- // Apply step
81- if ( step > 0 ) {
82- newValue = Math . round ( newValue / step ) * step ;
83- }
80+ const { left, width } = sliderRef . current . getBoundingClientRect ( ) ;
81+ let newValue = min + ( ( clientXPos - left ) / width ) * ( max - min ) ;
8482
85- // Clamp to bounds
86- newValue = Math . min ( Math . max ( newValue , min ) , max ) ;
83+ if ( step > 0 ) {
84+ newValue = Math . round ( newValue / step ) * step ;
85+ }
8786
88- setInternalValue ( newValue ) ;
89- onValueChange ?.( newValue ) ;
90- clientX . jump ( e . clientX ) ;
91- } ;
87+ newValue = Math . min ( Math . max ( newValue , min ) , max ) ;
88+ setInternalValue ( newValue ) ;
89+ onValueChange ?.( newValue ) ;
90+ clientX . jump ( clientXPos ) ;
91+ } ,
92+ [ min , max , step , onValueChange , clientX ]
93+ ) ;
9294
9395 const handlePointerDown = ( e : React . PointerEvent < HTMLDivElement > ) => {
94- if ( disabled ) {
95- return ;
96- }
96+ if ( disabled ) return ;
9797
9898 setIsDragging ( true ) ;
99- handlePointerMove ( e ) ;
99+ updateValue ( e . clientX ) ;
100100 e . currentTarget . setPointerCapture ( e . pointerId ) ;
101+ document . body . style . cursor = 'grabbing' ;
102+ } ;
103+
104+ const handlePointerMove = ( e : React . PointerEvent < HTMLDivElement > ) => {
105+ if ( ! isDragging || disabled ) return ;
106+ updateValue ( e . clientX ) ;
101107 } ;
102108
103109 const handlePointerUp = ( ) => {
104110 setIsDragging ( false ) ;
105111 setRegion ( 'middle' ) ;
106112 overflow . jump ( 0 ) ;
107- } ;
108-
109- const getPercentage = ( ) : number => {
110- const range = max - min ;
111- if ( range === 0 ) {
112- return 0 ;
113- }
114- return ( ( internalValue - min ) / range ) * 100 ;
113+ document . body . style . cursor = '' ;
115114 } ;
116115
117116 return (
118117 < div className = { cn ( 'space-y-3' , className ) } >
119118 < div
120119 className = { cn (
121- 'flex items-center gap-4' ,
120+ 'flex select-none items-center gap-4' ,
122121 disabled && 'cursor-not-allowed opacity-50'
123122 ) }
124123 >
125- { /* Left Icon */ }
126124 < motion . div
127125 className = "shrink-0 text-muted-foreground"
128126 style = { {
@@ -135,62 +133,54 @@ export function Slider({
135133 { leftIcon }
136134 </ motion . div >
137135
138- { /* Slider Track */ }
139136 < div
137+ ref = { sliderRef }
140138 className = { cn (
141- 'relative flex-1 cursor-pointer touch-none' ,
142- disabled && 'cursor-not-allowed'
139+ 'relative flex-1 touch-none' ,
140+ disabled ? 'cursor-not-allowed' : 'cursor-grab' ,
141+ isDragging && 'cursor-grabbing'
143142 ) }
144143 onPointerDown = { handlePointerDown }
145- onPointerLeave = { handlePointerUp }
146144 onPointerMove = { handlePointerMove }
147145 onPointerUp = { handlePointerUp }
148- ref = { sliderRef }
146+ onPointerLeave = { handlePointerUp }
149147 >
150148 < motion . div
151149 className = "relative"
152150 style = { {
153151 scaleX : useTransform ( ( ) => {
154- if ( sliderRef . current ) {
155- const { width } = sliderRef . current . getBoundingClientRect ( ) ;
156- return 1 + overflow . get ( ) / width ;
157- }
158- return 1 ;
152+ if ( ! sliderRef . current ) return 1 ;
153+ const { width } = sliderRef . current . getBoundingClientRect ( ) ;
154+ return 1 + overflow . get ( ) / width ;
159155 } ) ,
160156 scaleY : useTransform ( overflow , [ 0 , MAX_OVERFLOW ] , [ 1 , 0.7 ] ) ,
161157 transformOrigin : useTransform ( ( ) => {
162- if ( sliderRef . current ) {
163- const { left, width } =
164- sliderRef . current . getBoundingClientRect ( ) ;
165- return clientX . get ( ) < left + width / 2 ? 'right' : 'left' ;
166- }
167- return 'center' ;
158+ if ( ! sliderRef . current ) return 'center' ;
159+ const { left, width } =
160+ sliderRef . current . getBoundingClientRect ( ) ;
161+ return clientX . get ( ) < left + width / 2 ? 'right' : 'left' ;
168162 } ) ,
169163 } }
170164 >
171- { /* Track Background */ }
172165 < div className = "h-2 w-full rounded-full bg-secondary" >
173- { /* Progress */ }
174166 < div
175- className = "h-full rounded-full bg-primary"
176- style = { { width : `${ getPercentage ( ) } %` } }
167+ className = "h-full rounded-full bg-primary transition-[width] duration-75 "
168+ style = { { width : `${ percentage } %` } }
177169 />
178170 </ div >
179171
180- { /* Thumb */ }
181172 < motion . div
182- className = "-translate-y-1/2 absolute top-1/2 h-4 w-4 rounded-full border-2 border-primary bg-background shadow-sm"
183- style = { {
184- left : ` ${ getPercentage ( ) } %` ,
185- x : '-50%' ,
186- } }
173+ className = { cn (
174+ '-translate-y-1/2 absolute top-1/2 size-4 rounded-full border-2 border-primary bg-background shadow-sm' ,
175+ isDragging ? 'cursor-grabbing' : 'cursor-grab'
176+ ) }
177+ style = { { left : ` ${ percentage } %` , x : '-50%' } }
187178 whileHover = { { scale : 1.1 } }
188179 whileTap = { { scale : 0.95 } }
189180 />
190181 </ motion . div >
191182 </ div >
192183
193- { /* Right Icon */ }
194184 < motion . div
195185 className = "shrink-0 text-muted-foreground"
196186 style = { {
@@ -204,10 +194,9 @@ export function Slider({
204194 </ motion . div >
205195 </ div >
206196
207- { /* Value Display */ }
208197 { showValue && (
209198 < div className = "text-center" >
210- < span className = "font-medium font-mono text-sm" >
199+ < span className = "font-medium font-mono text-sm tabular-nums " >
211200 { Math . round ( internalValue ) }
212201 { max === 100 && '%' }
213202 </ span >
@@ -216,13 +205,3 @@ export function Slider({
216205 </ div >
217206 ) ;
218207}
219-
220- // Decay function for smooth overflow animation
221- function decay ( value : number , max : number ) : number {
222- if ( max === 0 ) {
223- return 0 ;
224- }
225- const entry = value / max ;
226- const sigmoid = 2 * ( 1 / ( 1 + Math . exp ( - entry ) ) - 0.5 ) ;
227- return sigmoid * max ;
228- }
0 commit comments