@moontra/moonui 2.0.7 → 2.0.9

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.
Files changed (94) hide show
  1. package/package.json +26 -19
  2. package/src/__tests__/use-intersection-observer.test.tsx +216 -0
  3. package/src/__tests__/use-local-storage.test.tsx +174 -0
  4. package/src/__tests__/use-pro-access.test.tsx +183 -0
  5. package/src/components/ui/__tests__/accordion.test.tsx +571 -0
  6. package/src/components/ui/__tests__/alert.test.tsx +484 -0
  7. package/src/components/ui/__tests__/aspect-ratio.test.tsx +492 -0
  8. package/src/components/ui/__tests__/avatar.test.tsx +382 -0
  9. package/src/components/ui/__tests__/badge.test.tsx +364 -0
  10. package/src/components/ui/__tests__/breadcrumb.test.tsx +687 -0
  11. package/src/components/ui/__tests__/button.test.tsx +91 -0
  12. package/src/components/ui/__tests__/card.test.tsx +104 -0
  13. package/src/components/ui/__tests__/checkbox.test.tsx +591 -0
  14. package/src/components/ui/__tests__/collapsible.test.tsx +592 -0
  15. package/src/components/ui/__tests__/color-picker.test.tsx +365 -0
  16. package/src/components/ui/__tests__/command.test.tsx +677 -0
  17. package/src/components/ui/__tests__/data-table.test.tsx +256 -0
  18. package/src/components/ui/__tests__/date-picker.test.tsx +320 -0
  19. package/src/components/ui/__tests__/dialog.test.tsx +764 -0
  20. package/src/components/ui/__tests__/input.test.tsx +318 -0
  21. package/src/components/ui/__tests__/label.test.tsx +103 -0
  22. package/src/components/ui/__tests__/popover.test.tsx +637 -0
  23. package/src/components/ui/__tests__/progress.test.tsx +382 -0
  24. package/src/components/ui/__tests__/radio-group.test.tsx +555 -0
  25. package/src/components/ui/__tests__/select.test.tsx +705 -0
  26. package/src/components/ui/__tests__/skeleton.test.tsx +253 -0
  27. package/src/components/ui/__tests__/slider.test.tsx +493 -0
  28. package/src/components/ui/__tests__/switch.test.tsx +372 -0
  29. package/src/components/ui/__tests__/table.test.tsx +824 -0
  30. package/src/components/ui/__tests__/tabs.test.tsx +887 -0
  31. package/src/components/ui/__tests__/toast.test.tsx +458 -0
  32. package/src/components/ui/__tests__/tooltip.test.tsx +583 -0
  33. package/src/components/ui/accordion.tsx +63 -0
  34. package/src/components/ui/alert.tsx +130 -0
  35. package/src/components/ui/aspect-ratio.tsx +72 -0
  36. package/src/components/ui/avatar.tsx +135 -0
  37. package/src/components/ui/badge.tsx +225 -0
  38. package/src/components/ui/breadcrumb.tsx +207 -0
  39. package/src/components/ui/button.stories.tsx +162 -0
  40. package/src/components/ui/button.tsx +221 -0
  41. package/src/components/ui/calendar.tsx +262 -0
  42. package/src/components/ui/card.stories.tsx +208 -0
  43. package/src/components/ui/card.tsx +141 -0
  44. package/src/components/ui/checkbox.tsx +256 -0
  45. package/src/components/ui/collapsible.tsx +129 -0
  46. package/src/components/ui/color-picker.tsx +664 -0
  47. package/src/components/ui/command.tsx +218 -0
  48. package/src/components/ui/data-table.stories.tsx +509 -0
  49. package/src/components/ui/data-table.tsx +623 -0
  50. package/src/components/ui/date-picker.stories.tsx +321 -0
  51. package/src/components/ui/date-picker.tsx +603 -0
  52. package/src/components/ui/dialog.tsx +332 -0
  53. package/src/components/ui/dropdown-menu.tsx +200 -0
  54. package/src/components/ui/file-upload.tsx +400 -0
  55. package/src/components/ui/github-stars.tsx +201 -0
  56. package/src/components/ui/index.ts +12 -0
  57. package/src/components/ui/input.stories.tsx +255 -0
  58. package/src/components/ui/input.tsx +219 -0
  59. package/src/components/ui/label.tsx +26 -0
  60. package/src/components/ui/locked-component.tsx +215 -0
  61. package/src/components/ui/moon-logo.tsx +97 -0
  62. package/src/components/ui/pagination.tsx +116 -0
  63. package/src/components/ui/popover.tsx +183 -0
  64. package/src/components/ui/progress.tsx +182 -0
  65. package/src/components/ui/radio-group.tsx +243 -0
  66. package/src/components/ui/select.tsx +273 -0
  67. package/src/components/ui/separator.tsx +140 -0
  68. package/src/components/ui/simple-editor.tsx +652 -0
  69. package/src/components/ui/skeleton.tsx +351 -0
  70. package/src/components/ui/slider.tsx +351 -0
  71. package/src/components/ui/switch.tsx +83 -0
  72. package/src/components/ui/table.tsx +322 -0
  73. package/src/components/ui/tabs.tsx +195 -0
  74. package/src/components/ui/textarea.tsx +46 -0
  75. package/src/components/ui/toast.tsx +313 -0
  76. package/src/components/ui/toggle.tsx +47 -0
  77. package/src/components/ui/tooltip.tsx +152 -0
  78. package/src/docs/theme-system.md +1194 -0
  79. package/src/index.tsx +39 -0
  80. package/src/lib/micro-interactions.ts +255 -0
  81. package/src/lib/theme.tsx +398 -0
  82. package/src/lib/utils.ts +69 -0
  83. package/src/styles/design-system.css +365 -0
  84. package/src/styles/index.css +4 -0
  85. package/src/styles/tailwind.css +6 -0
  86. package/src/styles/tokens.css +453 -0
  87. package/src/use-intersection-observer.tsx +154 -0
  88. package/src/use-local-storage.tsx +71 -0
  89. package/src/use-paddle.ts +138 -0
  90. package/src/use-performance-optimizer.ts +379 -0
  91. package/src/use-pro-access.ts +141 -0
  92. package/src/use-scroll-animation.ts +221 -0
  93. package/src/use-subscription.ts +37 -0
  94. package/src/use-toast.ts +32 -0
@@ -0,0 +1,351 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { cva, type VariantProps } from "class-variance-authority";
5
+ import { cn } from "../../lib/utils";
6
+ import { motion } from "framer-motion";
7
+ import { skeletonAnimation } from "@/lib/micro-interactions";
8
+
9
+ /**
10
+ * Skeleton Component
11
+ *
12
+ * A loading placeholder that mimics the structure of content.
13
+ * Fully integrated with the theme system and provides various shapes, sizes, and animations.
14
+ */
15
+
16
+ const skeletonVariants = cva(
17
+ "animate-pulse rounded-md",
18
+ {
19
+ variants: {
20
+ variant: {
21
+ default: "bg-muted",
22
+ primary: "bg-primary/10",
23
+ secondary: "bg-secondary/10",
24
+ accent: "bg-accent",
25
+ },
26
+ size: {
27
+ default: "",
28
+ sm: "scale-90",
29
+ lg: "scale-110",
30
+ },
31
+ shape: {
32
+ rect: "rounded-md",
33
+ circle: "rounded-full",
34
+ pill: "rounded-full",
35
+ },
36
+ animation: {
37
+ pulse: "animate-pulse",
38
+ wave: "overflow-hidden relative after:absolute after:inset-0 after:-translate-x-full after:animate-[shimmer_2s_infinite] after:bg-gradient-to-r after:from-transparent after:via-white/10 after:to-transparent dark:after:via-white/5",
39
+ none: "animate-none",
40
+ },
41
+ },
42
+ defaultVariants: {
43
+ variant: "default",
44
+ size: "default",
45
+ shape: "rect",
46
+ animation: "pulse",
47
+ },
48
+ }
49
+ );
50
+
51
+ export interface SkeletonProps
52
+ extends React.HTMLAttributes<HTMLDivElement>,
53
+ VariantProps<typeof skeletonVariants> {
54
+ /**
55
+ * Height of the skeleton
56
+ */
57
+ height?: string | number;
58
+ /**
59
+ * Width of the skeleton
60
+ */
61
+ width?: string | number;
62
+ /**
63
+ * Whether the content is loaded
64
+ */
65
+ isLoaded?: boolean;
66
+ /**
67
+ * Content to show when loaded
68
+ */
69
+ children?: React.ReactNode;
70
+ /**
71
+ * Fade-in animation duration in ms
72
+ */
73
+ fadeInDuration?: number;
74
+ /**
75
+ * Use motion animation for loading state
76
+ */
77
+ motion?: boolean;
78
+ }
79
+
80
+ export const Skeleton = React.forwardRef<HTMLDivElement, SkeletonProps>(
81
+ ({
82
+ className,
83
+ variant,
84
+ size,
85
+ shape,
86
+ animation,
87
+ height,
88
+ width,
89
+ isLoaded = false,
90
+ children,
91
+ fadeInDuration = 400,
92
+ motion: useMotion = false,
93
+ style,
94
+ ...props
95
+ }, ref) => {
96
+ // Show content when loaded
97
+ if (isLoaded && children) {
98
+ if (useMotion) {
99
+ return (
100
+ <motion.div
101
+ ref={ref}
102
+ initial={{ opacity: 0 }}
103
+ animate={{ opacity: 1 }}
104
+ transition={{ duration: fadeInDuration / 1000 }}
105
+ className={className}
106
+ style={{
107
+ height,
108
+ width,
109
+ ...style,
110
+ }}
111
+ {...props}
112
+ >
113
+ {children}
114
+ </motion.div>
115
+ );
116
+ }
117
+
118
+ return (
119
+ <div
120
+ ref={ref}
121
+ className={cn("animate-fade-in", className)}
122
+ style={{
123
+ animationDuration: `${fadeInDuration}ms`,
124
+ height,
125
+ width,
126
+ ...style,
127
+ }}
128
+ {...props}
129
+ >
130
+ {children}
131
+ </div>
132
+ );
133
+ }
134
+
135
+ const SkeletonElement = useMotion ? motion.div : "div";
136
+
137
+ return (
138
+ <SkeletonElement
139
+ ref={ref}
140
+ className={cn(
141
+ skeletonVariants({ variant, size, shape, animation }),
142
+ "relative isolate",
143
+ className
144
+ )}
145
+ style={{
146
+ height,
147
+ width,
148
+ ...style,
149
+ }}
150
+ {...(useMotion ? skeletonAnimation : {})}
151
+ {...props}
152
+ />
153
+ );
154
+ }
155
+ );
156
+ Skeleton.displayName = "Skeleton";
157
+
158
+ /**
159
+ * Text Skeleton Component
160
+ *
161
+ * Creates skeleton loading states for text content
162
+ */
163
+ export interface SkeletonTextProps extends Omit<SkeletonProps, "children" | "isLoaded"> {
164
+ /**
165
+ * Kaç satır gösterileceği
166
+ */
167
+ lines?: number;
168
+ /**
169
+ * Satır yüksekliği
170
+ */
171
+ lineHeight?: string | number;
172
+ /**
173
+ * Satırlar arası boşluk
174
+ */
175
+ spacing?: string | number;
176
+ /**
177
+ * Son satırın genişlik yüzdesi (0-100)
178
+ */
179
+ lastLineWidth?: number;
180
+ /**
181
+ * Rastgele satır genişlikleri
182
+ */
183
+ randomWidths?: boolean;
184
+ }
185
+
186
+ export const SkeletonText = React.forwardRef<HTMLDivElement, SkeletonTextProps>(
187
+ ({
188
+ className,
189
+ lines = 3,
190
+ lineHeight = "0.85rem",
191
+ spacing = "0.5rem",
192
+ lastLineWidth = 80,
193
+ randomWidths = false,
194
+ variant = "default",
195
+ animation = "pulse",
196
+ ...props
197
+ }, ref) => {
198
+ return (
199
+ <div
200
+ ref={ref}
201
+ className={cn("flex flex-col", className)}
202
+ style={{ gap: spacing }}
203
+ {...props}
204
+ >
205
+ {Array.from({ length: lines }).map((_, i) => {
206
+ const isLastLine = i === lines - 1;
207
+ // Use deterministic widths based on line index for SSR compatibility
208
+ const widthPercentage = isLastLine
209
+ ? lastLineWidth
210
+ : randomWidths
211
+ ? 85 + (i % 3) * 5 // Creates a pattern: 85%, 90%, 95%, 85%, ...
212
+ : 100;
213
+
214
+ return (
215
+ <Skeleton
216
+ key={i}
217
+ variant={variant}
218
+ animation={animation}
219
+ className="w-full"
220
+ style={{
221
+ height: lineHeight,
222
+ width: `${widthPercentage}%`,
223
+ }}
224
+ />
225
+ );
226
+ })}
227
+ </div>
228
+ );
229
+ }
230
+ );
231
+ SkeletonText.displayName = "SkeletonText";
232
+
233
+ /**
234
+ * Avatar Skeleton Component
235
+ */
236
+ export interface SkeletonAvatarProps extends Omit<SkeletonProps, "shape" | "size"> {
237
+ /**
238
+ * Avatar boyutu (piksel)
239
+ */
240
+ size?: string | number;
241
+ }
242
+
243
+ export const SkeletonAvatar = React.forwardRef<HTMLDivElement, SkeletonAvatarProps>(
244
+ ({
245
+ className,
246
+ size = "2.5rem",
247
+ variant = "default",
248
+ animation = "pulse",
249
+ ...props
250
+ }, ref) => {
251
+ return (
252
+ <Skeleton
253
+ ref={ref}
254
+ variant={variant}
255
+ animation={animation}
256
+ shape="circle"
257
+ className={cn("shrink-0", className)}
258
+ style={{
259
+ height: size,
260
+ width: size,
261
+ }}
262
+ {...props}
263
+ />
264
+ );
265
+ }
266
+ );
267
+ SkeletonAvatar.displayName = "SkeletonAvatar";
268
+
269
+ /**
270
+ * Card Skeleton Component
271
+ */
272
+ export interface SkeletonCardProps extends Omit<SkeletonProps, "children" | "isLoaded"> {
273
+ /**
274
+ * Kart başlığının gösterilip gösterilmeyeceği
275
+ */
276
+ showHeader?: boolean;
277
+ /**
278
+ * İçerik satır sayısı
279
+ */
280
+ contentLines?: number;
281
+ /**
282
+ * Alt bilgi gösterilip gösterilmeyeceği
283
+ */
284
+ showFooter?: boolean;
285
+ }
286
+
287
+ export const SkeletonCard = React.forwardRef<HTMLDivElement, SkeletonCardProps>(
288
+ ({
289
+ className,
290
+ showHeader = true,
291
+ contentLines = 3,
292
+ showFooter = true,
293
+ variant = "default",
294
+ animation = "pulse",
295
+ ...props
296
+ }, ref) => {
297
+ return (
298
+ <div
299
+ ref={ref}
300
+ className={cn(
301
+ "overflow-hidden rounded-md border border-border bg-background p-4",
302
+ className
303
+ )}
304
+ {...props}
305
+ >
306
+ {showHeader && (
307
+ <div className="mb-4 flex items-center gap-4">
308
+ <SkeletonAvatar size="2.5rem" variant={variant} animation={animation} />
309
+ <div className="flex-1">
310
+ <Skeleton
311
+ variant={variant}
312
+ animation={animation}
313
+ className="mb-2 h-4 w-1/3"
314
+ />
315
+ <Skeleton
316
+ variant={variant}
317
+ animation={animation}
318
+ className="h-3 w-1/4"
319
+ />
320
+ </div>
321
+ </div>
322
+ )}
323
+
324
+ <SkeletonText
325
+ lines={contentLines}
326
+ variant={variant}
327
+ animation={animation}
328
+ className="mb-4"
329
+ />
330
+
331
+ {showFooter && (
332
+ <div className="flex items-center justify-between mt-4 pt-4 border-t border-border">
333
+ <Skeleton
334
+ variant={variant}
335
+ animation={animation}
336
+ className="h-4 w-1/4"
337
+ />
338
+ <Skeleton
339
+ variant={variant}
340
+ animation={animation}
341
+ className="h-4 w-1/5"
342
+ />
343
+ </div>
344
+ )}
345
+ </div>
346
+ );
347
+ }
348
+ );
349
+ SkeletonCard.displayName = "SkeletonCard";
350
+
351
+ export { skeletonVariants };
@@ -0,0 +1,351 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cva, type VariantProps } from "class-variance-authority"
5
+
6
+ import { cn } from "../../lib/utils"
7
+
8
+ /**
9
+ * Slider Component
10
+ *
11
+ * Accessible, customizable slider component fully integrated with the theme system.
12
+ * Used for value ranges like volume, brightness, price ranges.
13
+ */
14
+
15
+ const sliderVariants = cva(
16
+ "relative flex w-full touch-none select-none items-center",
17
+ {
18
+ variants: {
19
+ size: {
20
+ sm: "h-5",
21
+ default: "h-6",
22
+ md: "h-8",
23
+ lg: "h-10",
24
+ },
25
+ },
26
+ defaultVariants: {
27
+ size: "default",
28
+ },
29
+ }
30
+ )
31
+
32
+ const sliderTrackVariants = cva(
33
+ "relative h-1.5 w-full grow overflow-hidden rounded-full",
34
+ {
35
+ variants: {
36
+ variant: {
37
+ default: "bg-muted",
38
+ primary: "bg-primary/20",
39
+ secondary: "bg-secondary/20",
40
+ accent: "bg-accent/20",
41
+ success: "bg-success/20",
42
+ warning: "bg-warning/20",
43
+ error: "bg-error/20",
44
+ },
45
+ },
46
+ defaultVariants: {
47
+ variant: "default",
48
+ },
49
+ }
50
+ )
51
+
52
+ const sliderRangeVariants = cva(
53
+ "absolute h-full",
54
+ {
55
+ variants: {
56
+ variant: {
57
+ default: "bg-foreground",
58
+ primary: "bg-primary",
59
+ secondary: "bg-secondary",
60
+ accent: "bg-accent",
61
+ success: "bg-success",
62
+ warning: "bg-warning",
63
+ error: "bg-error",
64
+ },
65
+ },
66
+ defaultVariants: {
67
+ variant: "primary",
68
+ },
69
+ }
70
+ )
71
+
72
+ const sliderThumbVariants = cva(
73
+ "block rounded-full border-2 ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
74
+ {
75
+ variants: {
76
+ variant: {
77
+ default: "border-foreground bg-background",
78
+ primary: "border-primary bg-background",
79
+ secondary: "border-secondary bg-background",
80
+ accent: "border-accent bg-background",
81
+ success: "border-success bg-background",
82
+ warning: "border-warning bg-background",
83
+ error: "border-error bg-background",
84
+ },
85
+ size: {
86
+ sm: "h-3 w-3",
87
+ default: "h-4 w-4",
88
+ md: "h-5 w-5",
89
+ lg: "h-6 w-6",
90
+ },
91
+ },
92
+ defaultVariants: {
93
+ variant: "primary",
94
+ size: "default",
95
+ },
96
+ }
97
+ )
98
+
99
+ // Custom type definition for component properties
100
+ type SliderBaseProps = {
101
+ /**
102
+ * Track variant
103
+ */
104
+ trackVariant?: VariantProps<typeof sliderTrackVariants>["variant"];
105
+ /**
106
+ * Range variant
107
+ */
108
+ rangeVariant?: VariantProps<typeof sliderRangeVariants>["variant"];
109
+ /**
110
+ * Thumb variant
111
+ */
112
+ thumbVariant?: VariantProps<typeof sliderThumbVariants>["variant"];
113
+ /**
114
+ * Thumb size
115
+ */
116
+ thumbSize?: VariantProps<typeof sliderThumbVariants>["size"];
117
+ /**
118
+ * Show value label
119
+ */
120
+ showValueLabel?: boolean;
121
+ /**
122
+ * Value label format function
123
+ */
124
+ valueLabelFormat?: (value: number) => string;
125
+ /**
126
+ * Value label class name
127
+ */
128
+ valueLabelClassName?: string;
129
+ /**
130
+ * Slider value
131
+ */
132
+ value?: number[];
133
+ /**
134
+ * Default value
135
+ */
136
+ defaultValue?: number[];
137
+ /**
138
+ * Function called when value changes
139
+ */
140
+ onValueChange?: (value: number[]) => void;
141
+ /**
142
+ * Minimum value
143
+ */
144
+ min?: number;
145
+ /**
146
+ * Maximum value
147
+ */
148
+ max?: number;
149
+ /**
150
+ * Step value
151
+ */
152
+ step?: number;
153
+ /**
154
+ * Slider size
155
+ */
156
+ size?: VariantProps<typeof sliderVariants>["size"];
157
+ /**
158
+ * Disabled state
159
+ */
160
+ disabled?: boolean;
161
+ }
162
+
163
+ // Merge HTML properties without defaultValue conflicts
164
+ type SliderProps = SliderBaseProps & Omit<React.HTMLAttributes<HTMLDivElement>, 'defaultValue'>
165
+
166
+ const Slider = React.forwardRef<
167
+ HTMLDivElement,
168
+ SliderProps
169
+ >(({
170
+ className,
171
+ size,
172
+ trackVariant,
173
+ rangeVariant,
174
+ thumbVariant,
175
+ thumbSize,
176
+ showValueLabel = false,
177
+ valueLabelFormat,
178
+ valueLabelClassName,
179
+ value,
180
+ defaultValue = [0],
181
+ onValueChange,
182
+ disabled,
183
+ ...props
184
+ }, ref) => {
185
+ // Value management
186
+ const [sliderValue, setSliderValue] = React.useState<number[]>(
187
+ value as number[] || defaultValue as number[] || [0]
188
+ );
189
+
190
+ React.useEffect(() => {
191
+ if (value !== undefined) {
192
+ setSliderValue(value as number[]);
193
+ }
194
+ }, [value]);
195
+
196
+ // Format value label with proper precision handling
197
+ const formatValue = (val: number) => {
198
+ if (valueLabelFormat) {
199
+ return valueLabelFormat(val);
200
+ }
201
+ // Round to avoid floating point precision issues
202
+ const roundedValue = Math.round(val * 100) / 100;
203
+ return `${roundedValue}`;
204
+ };
205
+
206
+ // Calculate percentage for thumb position
207
+ const calculateThumbPercent = (value: number, min: number, max: number) => {
208
+ return ((value - min) / (max - min)) * 100;
209
+ };
210
+
211
+ // Handle mouse/touch events
212
+ const trackRef = React.useRef<HTMLDivElement>(null);
213
+
214
+ const min = props.min || 0;
215
+ const max = props.max || 100;
216
+ const step = props.step || 1;
217
+
218
+ // Handle value change with proper precision
219
+ const handleValueChange = (newValues: number[]) => {
220
+ // Round values to avoid floating point precision issues
221
+ const roundedValues = newValues.map(val => {
222
+ if (step < 1) {
223
+ // For decimal steps, round to appropriate decimal places
224
+ const decimals = step.toString().split('.')[1]?.length || 0;
225
+ return Math.round(val * Math.pow(10, decimals)) / Math.pow(10, decimals);
226
+ }
227
+ return Math.round(val);
228
+ });
229
+
230
+ setSliderValue(roundedValues);
231
+ if (onValueChange) {
232
+ onValueChange(roundedValues);
233
+ }
234
+ };
235
+
236
+ // Handle track click
237
+ const handleTrackClick = (event: React.MouseEvent<HTMLDivElement>) => {
238
+ if (disabled) return;
239
+
240
+ const track = trackRef.current;
241
+ if (!track) return;
242
+
243
+ const rect = track.getBoundingClientRect();
244
+ const percent = (event.clientX - rect.left) / rect.width;
245
+ const rawValue = min + percent * (max - min);
246
+ const steppedValue = Math.round(rawValue / step) * step;
247
+ const boundedValue = Math.max(min, Math.min(max, steppedValue));
248
+
249
+ const newValues = [...sliderValue];
250
+ // Just update the first thumb for simplicity
251
+ newValues[0] = boundedValue;
252
+ handleValueChange(newValues);
253
+ };
254
+
255
+ const handleThumbMouseDown = (index: number) => (event: React.MouseEvent) => {
256
+ if (disabled) return;
257
+ event.preventDefault();
258
+
259
+ const handleMouseMove = (moveEvent: MouseEvent) => {
260
+ const track = trackRef.current;
261
+ if (!track) return;
262
+
263
+ const rect = track.getBoundingClientRect();
264
+ const percent = (moveEvent.clientX - rect.left) / rect.width;
265
+ const rawValue = min + percent * (max - min);
266
+ const steppedValue = Math.round(rawValue / step) * step;
267
+ const boundedValue = Math.max(min, Math.min(max, steppedValue));
268
+
269
+ const newValues = [...sliderValue];
270
+ newValues[index] = boundedValue;
271
+ handleValueChange(newValues);
272
+ };
273
+
274
+ const handleMouseUp = () => {
275
+ document.removeEventListener('mousemove', handleMouseMove);
276
+ document.removeEventListener('mouseup', handleMouseUp);
277
+ };
278
+
279
+ document.addEventListener('mousemove', handleMouseMove);
280
+ document.addEventListener('mouseup', handleMouseUp);
281
+ };
282
+
283
+ return (
284
+ <div className="w-full" ref={ref} {...props}>
285
+ {showValueLabel && (
286
+ <div className={cn(
287
+ "flex justify-end mb-1 text-sm text-muted-foreground",
288
+ valueLabelClassName
289
+ )}>
290
+ {sliderValue.length === 1 ? (
291
+ <span>{formatValue(sliderValue[0])}</span>
292
+ ) : (
293
+ <span>{formatValue(sliderValue[0])} - {formatValue(sliderValue[sliderValue.length - 1])}</span>
294
+ )}
295
+ </div>
296
+ )}
297
+
298
+ <div
299
+ className={cn(sliderVariants({ size }), className)}
300
+ data-disabled={disabled ? true : undefined}
301
+ >
302
+ {/* Track */}
303
+ <div
304
+ ref={trackRef}
305
+ className={cn(sliderTrackVariants({ variant: trackVariant || thumbVariant || "default" }))}
306
+ onClick={handleTrackClick}
307
+ >
308
+ {/* Range */}
309
+ <div
310
+ className={cn(sliderRangeVariants({ variant: rangeVariant || thumbVariant || "primary" }))}
311
+ style={{
312
+ left: '0%',
313
+ width: `${calculateThumbPercent(sliderValue[0], min, max)}%`,
314
+ }}
315
+ />
316
+ </div>
317
+
318
+ {/* Thumbs */}
319
+ {sliderValue.map((value, i) => (
320
+ <div
321
+ key={i}
322
+ className={cn(sliderThumbVariants({
323
+ variant: thumbVariant || "primary",
324
+ size: thumbSize
325
+ }))}
326
+ style={{
327
+ left: `${calculateThumbPercent(value, min, max)}%`,
328
+ transform: 'translateX(-50%)',
329
+ position: 'absolute',
330
+ top: '50%',
331
+ marginTop: thumbSize === "sm" ? '-6px' : thumbSize === "lg" ? '-12px' : '-8px',
332
+ cursor: disabled ? 'not-allowed' : 'pointer',
333
+ }}
334
+ onMouseDown={handleThumbMouseDown(i)}
335
+ aria-label={`Thumb ${i + 1}`}
336
+ tabIndex={disabled ? -1 : 0}
337
+ role="slider"
338
+ aria-valuemin={min}
339
+ aria-valuemax={max}
340
+ aria-valuenow={value}
341
+ aria-disabled={disabled}
342
+ />
343
+ ))}
344
+ </div>
345
+ </div>
346
+ )
347
+ })
348
+
349
+ Slider.displayName = "Slider"
350
+
351
+ export { Slider }