@srcroot/ui 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,505 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { PanelLeft } from "lucide-react"
5
+ import { cva, type VariantProps } from "class-variance-authority"
6
+
7
+ import { cn } from "@/lib/utils"
8
+ import { Button } from "@/components/ui/button"
9
+ import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet"
10
+
11
+ const SIDEBAR_WIDTH = "16rem"
12
+ const SIDEBAR_WIDTH_MOBILE = "18rem"
13
+ const SIDEBAR_WIDTH_ICON = "3rem"
14
+
15
+ const SidebarContext = React.createContext<{
16
+ state: "expanded" | "collapsed"
17
+ open: boolean
18
+ setOpen: (open: boolean) => void
19
+ openMobile: boolean
20
+ setOpenMobile: (open: boolean) => void
21
+ isMobile: boolean
22
+ toggleSidebar: () => void
23
+ } | null>(null)
24
+
25
+ function useSidebar() {
26
+ const context = React.useContext(SidebarContext)
27
+ if (!context) {
28
+ throw new Error("useSidebar must be used within a SidebarProvider")
29
+ }
30
+ return context
31
+ }
32
+
33
+ const SidebarProvider = React.forwardRef<
34
+ HTMLDivElement,
35
+ React.HTMLAttributes<HTMLDivElement> & {
36
+ defaultOpen?: boolean
37
+ open?: boolean
38
+ onOpenChange?: (open: boolean) => void
39
+ }
40
+ >(({ className, style, children, defaultOpen = true, open: openProp, onOpenChange: setOpenProp, ...props }, ref) => {
41
+ const [isMobile, setIsMobile] = React.useState(false)
42
+ const [openMobile, setOpenMobile] = React.useState(false)
43
+
44
+ // Using internal state for "expanded/collapsed" on desktop
45
+ const [_open, _setOpen] = React.useState(defaultOpen)
46
+ const open = openProp ?? _open
47
+ const setOpen = React.useCallback(
48
+ (value: boolean | ((value: boolean) => boolean)) => {
49
+ if (setOpenProp) {
50
+ return setOpenProp(typeof value === "function" ? value(open) : value)
51
+ }
52
+ _setOpen(value)
53
+ },
54
+ [setOpenProp, open]
55
+ )
56
+
57
+
58
+ // Collapsible state helper
59
+ const state = open ? "expanded" : "collapsed"
60
+
61
+ const toggleSidebar = React.useCallback(() => {
62
+ return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)
63
+ }, [isMobile, setOpen, setOpenMobile])
64
+
65
+
66
+ React.useEffect(() => {
67
+ const checkMobile = () => {
68
+ setIsMobile(window.innerWidth < 1024) // lg breakpoint
69
+ }
70
+ checkMobile()
71
+ window.addEventListener("resize", checkMobile)
72
+ return () => window.removeEventListener("resize", checkMobile)
73
+ }, [])
74
+
75
+ return (
76
+ <SidebarContext.Provider
77
+ value={{
78
+ state,
79
+ open,
80
+ setOpen,
81
+ isMobile,
82
+ openMobile,
83
+ setOpenMobile,
84
+ toggleSidebar,
85
+ }}
86
+ >
87
+ <div
88
+ style={
89
+ {
90
+ "--sidebar-width": SIDEBAR_WIDTH,
91
+ "--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
92
+ ...style,
93
+ } as React.CSSProperties
94
+ }
95
+ ref={ref}
96
+ className={cn(
97
+ "group/sidebar-wrapper flex min-h-screen w-full has-[[data-variant=inset]]:bg-sidebar",
98
+ className
99
+ )}
100
+ {...props}
101
+ >
102
+ {children}
103
+ </div>
104
+ </SidebarContext.Provider>
105
+ )
106
+ })
107
+ SidebarProvider.displayName = "SidebarProvider"
108
+
109
+ const Sidebar = React.forwardRef<
110
+ HTMLDivElement,
111
+ React.HTMLAttributes<HTMLDivElement> & {
112
+ side?: "left" | "right"
113
+ variant?: "sidebar" | "floating" | "inset"
114
+ collapsible?: "offcanvas" | "icon" | "none"
115
+ }
116
+ >(
117
+ (
118
+ {
119
+ side = "left",
120
+ variant = "sidebar",
121
+ collapsible = "offcanvas",
122
+ className,
123
+ children,
124
+ ...props
125
+ },
126
+ ref
127
+ ) => {
128
+ const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
129
+
130
+ if (collapsible === "none") {
131
+ return (
132
+ <div
133
+ className={cn(
134
+ "flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground",
135
+ className
136
+ )}
137
+ ref={ref}
138
+ {...props}
139
+ >
140
+ {children}
141
+ </div>
142
+ )
143
+ }
144
+
145
+ if (isMobile) {
146
+ return (
147
+ <Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
148
+ <SheetContent
149
+ data-sidebar="sidebar"
150
+ data-mobile="true"
151
+ className="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
152
+ style={
153
+ {
154
+ "--sidebar-width": SIDEBAR_WIDTH_MOBILE,
155
+ } as React.CSSProperties
156
+ }
157
+ side={side}
158
+ >
159
+ {/* Hidden Headers for Accessibility */}
160
+ <div className="sr-only">
161
+ <SheetHeader>
162
+ <SheetTitle>Menu</SheetTitle>
163
+ <SheetDescription>Navigation Menu</SheetDescription>
164
+ </SheetHeader>
165
+ </div>
166
+ <div className="flex h-full w-full flex-col">{children}</div>
167
+ </SheetContent>
168
+ </Sheet>
169
+ )
170
+ }
171
+
172
+ return (
173
+ <div
174
+ ref={ref}
175
+ className="group peer hidden md:block text-sidebar-foreground"
176
+ data-state={state}
177
+ data-collapsible={state === "collapsed" ? collapsible : ""}
178
+ data-variant={variant}
179
+ data-side={side}
180
+ >
181
+ {/* Visual Gap for Sidebar (Fixed placeholder) */}
182
+ <div
183
+ className={cn(
184
+ "duration-200 relative h-svh w-[--sidebar-width] bg-transparent transition-[width] ease-linear",
185
+ "group-data-[collapsible=offcanvas]:w-0",
186
+ "group-data-[side=right]:rotate-180",
187
+ variant === "floating" || variant === "inset"
188
+ ? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]"
189
+ : "group-data-[collapsible=icon]:w-[--sidebar-width-icon]"
190
+ )}
191
+ />
192
+
193
+ {/* Actual Fixed Sidebar */}
194
+ <div
195
+ className={cn(
196
+ "duration-200 fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] ease-linear md:flex",
197
+ side === "left"
198
+ ? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
199
+ : "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
200
+ // Adjustments for floating/inset
201
+ variant === "floating" || variant === "inset"
202
+ ? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+_2px)]"
203
+ : "group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l",
204
+ className
205
+ )}
206
+ {...props}
207
+ >
208
+ <div
209
+ data-sidebar="sidebar"
210
+ className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow"
211
+ >
212
+ {children}
213
+ </div>
214
+ </div>
215
+ </div>
216
+ )
217
+ }
218
+ )
219
+ Sidebar.displayName = "Sidebar"
220
+
221
+ const SidebarTrigger = React.forwardRef<
222
+ HTMLButtonElement,
223
+ React.ButtonHTMLAttributes<HTMLButtonElement>
224
+ >(({ className, onClick, ...props }, ref) => {
225
+ const { toggleSidebar } = useSidebar()
226
+
227
+ return (
228
+ <Button
229
+ ref={ref}
230
+ data-sidebar="trigger"
231
+ variant="ghost"
232
+ size="icon"
233
+ className={cn("h-7 w-7", className)}
234
+ onClick={(event: React.MouseEvent<HTMLButtonElement>) => {
235
+ onClick?.(event)
236
+ toggleSidebar()
237
+ }}
238
+ {...props}
239
+ >
240
+ <PanelLeft />
241
+ <span className="sr-only">Toggle Sidebar</span>
242
+ </Button>
243
+ )
244
+ })
245
+ SidebarTrigger.displayName = "SidebarTrigger"
246
+
247
+ const SidebarRail = React.forwardRef<
248
+ HTMLButtonElement,
249
+ React.ButtonHTMLAttributes<HTMLButtonElement>
250
+ >(({ className, ...props }, ref) => {
251
+ const { toggleSidebar } = useSidebar()
252
+
253
+ return (
254
+ <button
255
+ ref={ref}
256
+ data-sidebar="rail"
257
+ aria-label="Toggle Sidebar"
258
+ tabIndex={-1}
259
+ onClick={toggleSidebar}
260
+ title="Toggle Sidebar"
261
+ className={cn(
262
+ "absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex",
263
+ "[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize",
264
+ "[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
265
+ "group-data-[collapsible=offcanvas]:hover:bg-sidebar",
266
+ "group-data-[collapsible=icon]:w-2 group-data-[collapsible=icon]:group-data-[side=left]:-right-3 group-data-[collapsible=icon]:group-data-[side=right]:left-0",
267
+ className
268
+ )}
269
+ {...props}
270
+ />
271
+ )
272
+ })
273
+ SidebarRail.displayName = "SidebarRail"
274
+
275
+ const SidebarInset = React.forwardRef<
276
+ HTMLDivElement,
277
+ React.HTMLAttributes<HTMLDivElement>
278
+ >(({ className, ...props }, ref) => {
279
+ return (
280
+ <main
281
+ ref={ref}
282
+ className={cn(
283
+ "relative flex min-h-svh flex-1 flex-col bg-background",
284
+ "peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
285
+ className
286
+ )}
287
+ {...props}
288
+ />
289
+ )
290
+ })
291
+ SidebarInset.displayName = "SidebarInset"
292
+
293
+ const SidebarHeader = React.forwardRef<
294
+ HTMLDivElement,
295
+ React.HTMLAttributes<HTMLDivElement>
296
+ >(({ className, ...props }, ref) => (
297
+ <div
298
+ ref={ref}
299
+ data-sidebar="header"
300
+ className={cn("flex flex-col gap-2 p-2", className)}
301
+ {...props}
302
+ />
303
+ ))
304
+ SidebarHeader.displayName = "SidebarHeader"
305
+
306
+ const SidebarFooter = React.forwardRef<
307
+ HTMLDivElement,
308
+ React.HTMLAttributes<HTMLDivElement>
309
+ >(({ className, ...props }, ref) => (
310
+ <div
311
+ ref={ref}
312
+ data-sidebar="footer"
313
+ className={cn("flex flex-col gap-2 p-2", className)}
314
+ {...props}
315
+ />
316
+ ))
317
+ SidebarFooter.displayName = "SidebarFooter"
318
+
319
+ const SidebarContent = React.forwardRef<
320
+ HTMLDivElement,
321
+ React.HTMLAttributes<HTMLDivElement>
322
+ >(({ className, ...props }, ref) => (
323
+ <div
324
+ ref={ref}
325
+ data-sidebar="content"
326
+ className={cn(
327
+ "flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
328
+ className
329
+ )}
330
+ {...props}
331
+ />
332
+ ))
333
+ SidebarContent.displayName = "SidebarContent"
334
+
335
+ const SidebarGroup = React.forwardRef<
336
+ HTMLDivElement,
337
+ React.HTMLAttributes<HTMLDivElement>
338
+ >(({ className, ...props }, ref) => (
339
+ <div
340
+ ref={ref}
341
+ data-sidebar="group"
342
+ className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
343
+ {...props}
344
+ />
345
+ ))
346
+ SidebarGroup.displayName = "SidebarGroup"
347
+
348
+ const SidebarGroupLabel = React.forwardRef<
349
+ HTMLDivElement,
350
+ React.HTMLAttributes<HTMLDivElement> & { asChild?: boolean }
351
+ >(({ className, asChild = false, ...props }, ref) => {
352
+ // Only supporting div rendering for now
353
+ if (asChild) {
354
+ const child = React.Children.only(props.children) as React.ReactElement<any>
355
+ return React.cloneElement(child, {
356
+ ref,
357
+ "data-sidebar": "group-label",
358
+ className: cn(
359
+ "duration-200 flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opa] ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
360
+ "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
361
+ className,
362
+ child.props.className
363
+ ),
364
+ ...props,
365
+ children: child.props.children,
366
+ })
367
+ }
368
+
369
+ return (
370
+ <div
371
+ ref={ref}
372
+ data-sidebar="group-label"
373
+ className={cn(
374
+ "duration-200 flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opa] ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
375
+ "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
376
+ className
377
+ )}
378
+ {...props}
379
+ />
380
+ )
381
+ })
382
+ SidebarGroupLabel.displayName = "SidebarGroupLabel"
383
+
384
+ const SidebarGroupContent = React.forwardRef<
385
+ HTMLDivElement,
386
+ React.HTMLAttributes<HTMLDivElement>
387
+ >(({ className, ...props }, ref) => (
388
+ <div
389
+ ref={ref}
390
+ data-sidebar="group-content"
391
+ className={cn("w-full text-sm", className)}
392
+ {...props}
393
+ />
394
+ ))
395
+ SidebarGroupContent.displayName = "SidebarGroupContent"
396
+
397
+ const SidebarMenu = React.forwardRef<
398
+ HTMLUListElement,
399
+ React.HTMLAttributes<HTMLUListElement>
400
+ >(({ className, ...props }, ref) => (
401
+ <ul
402
+ ref={ref}
403
+ data-sidebar="menu"
404
+ className={cn("flex w-full min-w-0 flex-col gap-1", className)}
405
+ {...props}
406
+ />
407
+ ))
408
+ SidebarMenu.displayName = "SidebarMenu"
409
+
410
+ const SidebarMenuItem = React.forwardRef<
411
+ HTMLLIElement,
412
+ React.LiHTMLAttributes<HTMLLIElement>
413
+ >(({ className, ...props }, ref) => (
414
+ <li
415
+ ref={ref}
416
+ data-sidebar="menu-item"
417
+ className={cn("group/menu-item relative", className)}
418
+ {...props}
419
+ />
420
+ ))
421
+ SidebarMenuItem.displayName = "SidebarMenuItem"
422
+
423
+ const SidebarMenuButton = React.forwardRef<
424
+ HTMLButtonElement,
425
+ React.ButtonHTMLAttributes<HTMLButtonElement> & {
426
+ asChild?: boolean
427
+ isActive?: boolean
428
+ tooltip?: string | React.ComponentProps<any>
429
+ variant?: "default" | "ghost" | "outline" | "secondary" | "destructive" | "link" | null | undefined
430
+ size?: "default" | "sm" | "lg" | "icon" | null | undefined
431
+ }
432
+ >(
433
+ (
434
+ {
435
+ asChild = false,
436
+ isActive = false,
437
+ variant = "default",
438
+ size = "default",
439
+ tooltip,
440
+ className,
441
+ ...props
442
+ },
443
+ ref
444
+ ) => {
445
+ const Comp = "button"
446
+ // manual asChild handling
447
+ // const Comp = asChild ? Slot : "button"
448
+ // But we want to avoid Slot if possible per user request?
449
+ // Actually, if I can use the same cloneElement approach:
450
+
451
+ const buttonClass = cn(
452
+ "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
453
+ "data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground",
454
+ "data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground",
455
+ "group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:group-data-[collapsible=icon]:hidden",
456
+ className
457
+ )
458
+
459
+ // If tooltip is needed, we should implement it. For now, ignoring complexity of tooltip.
460
+
461
+ if (asChild) {
462
+ const child = React.Children.only(props.children) as React.ReactElement<any>
463
+
464
+ return React.cloneElement(child, {
465
+ ref,
466
+ className: cn(buttonClass, child.props.className),
467
+ "data-active": isActive,
468
+ "data-sidebar": "menu-button",
469
+ "data-size": size,
470
+ ...props,
471
+ children: child.props.children
472
+ })
473
+ }
474
+
475
+ return (
476
+ <button
477
+ ref={ref}
478
+ data-sidebar="menu-button"
479
+ data-size={size}
480
+ data-active={isActive}
481
+ className={buttonClass}
482
+ {...props}
483
+ />
484
+ )
485
+ }
486
+ )
487
+ SidebarMenuButton.displayName = "SidebarMenuButton"
488
+
489
+ export {
490
+ Sidebar,
491
+ SidebarContent,
492
+ SidebarFooter,
493
+ SidebarGroup,
494
+ SidebarGroupContent,
495
+ SidebarGroupLabel,
496
+ SidebarHeader,
497
+ SidebarInset,
498
+ SidebarMenu,
499
+ SidebarMenuButton,
500
+ SidebarMenuItem,
501
+ SidebarProvider,
502
+ SidebarRail,
503
+ SidebarTrigger,
504
+ useSidebar,
505
+ }
@@ -18,6 +18,8 @@ interface SliderProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChan
18
18
  * const [value, setValue] = useState([50])
19
19
  * <Slider value={value} onValueChange={setValue} max={100} step={1} />
20
20
  */
21
+ export { Slider }
22
+
21
23
  const Slider = React.forwardRef<HTMLDivElement, SliderProps>(
22
24
  ({
23
25
  className,
@@ -31,6 +33,8 @@ const Slider = React.forwardRef<HTMLDivElement, SliderProps>(
31
33
  ...props
32
34
  }, ref) => {
33
35
  const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue)
36
+ const trackRef = React.useRef<HTMLDivElement>(null)
37
+ const isDragging = React.useRef(false)
34
38
 
35
39
  const value = controlledValue !== undefined ? controlledValue : uncontrolledValue
36
40
  const setValue = onValueChange || setUncontrolledValue
@@ -38,6 +42,61 @@ const Slider = React.forwardRef<HTMLDivElement, SliderProps>(
38
42
 
39
43
  const percentage = ((currentValue - min) / (max - min)) * 100
40
44
 
45
+ const updateValue = (clientX: number) => {
46
+ if (!trackRef.current) return
47
+
48
+ const rect = trackRef.current.getBoundingClientRect()
49
+ const percentage = (clientX - rect.left) / rect.width
50
+ const rawValue = min + percentage * (max - min)
51
+ const steppedValue = Math.round(rawValue / step) * step
52
+ const clampedValue = Math.min(Math.max(steppedValue, min), max)
53
+
54
+ setValue([clampedValue])
55
+ }
56
+
57
+ const handleMouseDown = (e: React.MouseEvent) => {
58
+ if (disabled) return
59
+ isDragging.current = true
60
+ updateValue(e.clientX)
61
+
62
+ document.addEventListener('mousemove', handleMouseMove)
63
+ document.addEventListener('mouseup', handleMouseUp)
64
+ }
65
+
66
+ const handleMouseMove = (e: MouseEvent) => {
67
+ if (!isDragging.current) return
68
+ updateValue(e.clientX)
69
+ }
70
+
71
+ const handleMouseUp = () => {
72
+ isDragging.current = false
73
+ document.removeEventListener('mousemove', handleMouseMove)
74
+ document.removeEventListener('mouseup', handleMouseUp)
75
+ }
76
+
77
+ // Touch support
78
+ const handleTouchStart = (e: React.TouchEvent) => {
79
+ if (disabled) return
80
+ isDragging.current = true
81
+ updateValue(e.touches[0].clientX)
82
+
83
+ document.addEventListener('touchmove', handleTouchMove)
84
+ document.addEventListener('touchend', handleTouchEnd)
85
+ }
86
+
87
+ const handleTouchMove = (e: TouchEvent) => {
88
+ if (!isDragging.current) return
89
+ updateValue(e.touches[0].clientX)
90
+ e.preventDefault() // Prevent scrolling while dragging
91
+ }
92
+
93
+ const handleTouchEnd = () => {
94
+ isDragging.current = false
95
+ document.removeEventListener('touchmove', handleTouchMove)
96
+ document.removeEventListener('touchend', handleTouchEnd)
97
+ }
98
+
99
+
41
100
  const handleKeyDown = (e: React.KeyboardEvent) => {
42
101
  if (disabled) return
43
102
 
@@ -62,19 +121,21 @@ const Slider = React.forwardRef<HTMLDivElement, SliderProps>(
62
121
  return
63
122
  }
64
123
 
65
- e.preventDefault()
66
- setValue([newValue])
124
+ if (newValue !== currentValue) {
125
+ e.preventDefault()
126
+ setValue([newValue])
127
+ }
67
128
  }
68
129
 
69
- const handleTrackClick = (e: React.MouseEvent<HTMLDivElement>) => {
70
- if (disabled) return
71
-
72
- const rect = e.currentTarget.getBoundingClientRect()
73
- const clickPosition = (e.clientX - rect.left) / rect.width
74
- const newValue = min + clickPosition * (max - min)
75
- const steppedValue = Math.round(newValue / step) * step
76
- setValue([Math.min(Math.max(steppedValue, min), max)])
77
- }
130
+ // Cleanup on unmount
131
+ React.useEffect(() => {
132
+ return () => {
133
+ document.removeEventListener('mousemove', handleMouseMove)
134
+ document.removeEventListener('mouseup', handleMouseUp)
135
+ document.removeEventListener('touchmove', handleTouchMove)
136
+ document.removeEventListener('touchend', handleTouchEnd)
137
+ }
138
+ }, [])
78
139
 
79
140
  return (
80
141
  <div
@@ -86,24 +147,29 @@ const Slider = React.forwardRef<HTMLDivElement, SliderProps>(
86
147
  aria-disabled={disabled}
87
148
  tabIndex={disabled ? -1 : 0}
88
149
  className={cn(
89
- "relative flex w-full touch-none select-none items-center",
150
+ "relative flex w-full touch-none select-none items-center py-4 cursor-pointer", // Added vertical padding for easier touch area
90
151
  disabled && "opacity-50 cursor-not-allowed",
91
152
  className
92
153
  )}
93
154
  onKeyDown={handleKeyDown}
155
+ onMouseDown={handleMouseDown}
156
+ onTouchStart={handleTouchStart}
94
157
  {...props}
95
158
  >
96
159
  <div
97
- className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-primary/20 cursor-pointer"
98
- onClick={handleTrackClick}
160
+ ref={trackRef}
161
+ className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-primary/20"
99
162
  >
100
163
  <div
101
- className="absolute h-full bg-primary"
164
+ className="absolute h-full bg-primary transition-all duration-75 ease-out" // Added transition but very fast to feel responsive
102
165
  style={{ width: `${percentage}%` }}
103
166
  />
104
167
  </div>
105
168
  <div
106
- className="absolute block h-4 w-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
169
+ className={cn(
170
+ "absolute block h-4 w-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
171
+ "hover:bg-accent hover:border-primary" // Visual feedback
172
+ )}
107
173
  style={{ left: `calc(${percentage}% - 8px)` }}
108
174
  />
109
175
  </div>
@@ -111,5 +177,3 @@ const Slider = React.forwardRef<HTMLDivElement, SliderProps>(
111
177
  }
112
178
  )
113
179
  Slider.displayName = "Slider"
114
-
115
- export { Slider }