@srcroot/ui 0.0.54 → 0.0.56

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 (107) hide show
  1. package/README.md +151 -151
  2. package/dist/index.d.ts +0 -0
  3. package/dist/index.js +55 -1
  4. package/package.json +7 -2
  5. package/src/registry/analytics/google-analytics.tsx +36 -39
  6. package/src/registry/analytics/google-tag-manager.tsx +62 -65
  7. package/src/registry/analytics/meta-pixel.tsx +44 -47
  8. package/src/registry/analytics/microsoft-clarity.tsx +31 -34
  9. package/src/registry/analytics/tiktok-pixel.tsx +34 -37
  10. package/src/registry/lib/utils.ts +0 -0
  11. package/src/registry/themes/v3/blue.css +157 -157
  12. package/src/registry/themes/v3/glass.css +153 -153
  13. package/src/registry/themes/v3/gray.css +157 -157
  14. package/src/registry/themes/v3/green.css +157 -157
  15. package/src/registry/themes/v3/neutral.css +157 -157
  16. package/src/registry/themes/v3/orange.css +157 -157
  17. package/src/registry/themes/v3/rose.css +157 -157
  18. package/src/registry/themes/v3/slate.css +157 -157
  19. package/src/registry/themes/v3/stone.css +157 -157
  20. package/src/registry/themes/v3/violet.css +186 -186
  21. package/src/registry/themes/v3/zinc.css +157 -157
  22. package/src/registry/themes/v4/blue.css +184 -184
  23. package/src/registry/themes/v4/glass.css +180 -180
  24. package/src/registry/themes/v4/gray.css +184 -184
  25. package/src/registry/themes/v4/green.css +184 -184
  26. package/src/registry/themes/v4/neutral.css +184 -184
  27. package/src/registry/themes/v4/orange.css +184 -184
  28. package/src/registry/themes/v4/rose.css +184 -184
  29. package/src/registry/themes/v4/slate.css +184 -184
  30. package/src/registry/themes/v4/stone.css +184 -184
  31. package/src/registry/themes/v4/violet.css +184 -184
  32. package/src/registry/themes/v4/zinc.css +184 -184
  33. package/src/registry/ui/accordion.tsx +164 -165
  34. package/src/registry/ui/alert-dialog.tsx +213 -214
  35. package/src/registry/ui/alert.tsx +73 -76
  36. package/src/registry/ui/aspect-ratio.tsx +44 -47
  37. package/src/registry/ui/avatar.tsx +96 -97
  38. package/src/registry/ui/badge.tsx +52 -55
  39. package/src/registry/ui/breadcrumb.tsx +147 -150
  40. package/src/registry/ui/button-group.tsx +64 -67
  41. package/src/registry/ui/button.tsx +71 -72
  42. package/src/registry/ui/calendar.tsx +514 -515
  43. package/src/registry/ui/card.tsx +88 -91
  44. package/src/registry/ui/carousel.tsx +214 -214
  45. package/src/registry/ui/chart.tsx +373 -373
  46. package/src/registry/ui/chatbot.tsx +86 -13
  47. package/src/registry/ui/checkbox.tsx +93 -94
  48. package/src/registry/ui/collapsible.tsx +107 -108
  49. package/src/registry/ui/combobox.tsx +171 -171
  50. package/src/registry/ui/command.tsx +300 -300
  51. package/src/registry/ui/container.tsx +44 -47
  52. package/src/registry/ui/context-menu.tsx +221 -221
  53. package/src/registry/ui/date-picker.tsx +228 -228
  54. package/src/registry/ui/dialog.tsx +269 -270
  55. package/src/registry/ui/drawer.tsx +10 -4
  56. package/src/registry/ui/dropdown-menu.tsx +529 -530
  57. package/src/registry/ui/empty-state.tsx +0 -2
  58. package/src/registry/ui/file-upload.tsx +0 -0
  59. package/src/registry/ui/floating-dock.tsx +0 -0
  60. package/src/registry/ui/form-field.tsx +91 -94
  61. package/src/registry/ui/google-analytics.tsx +38 -0
  62. package/src/registry/ui/google-tag-manager.tsx +64 -0
  63. package/src/registry/ui/hover-card.tsx +223 -223
  64. package/src/registry/ui/image.tsx +144 -147
  65. package/src/registry/ui/input-group.tsx +82 -85
  66. package/src/registry/ui/input.tsx +125 -125
  67. package/src/registry/ui/kbd.tsx +60 -63
  68. package/src/registry/ui/label.tsx +36 -37
  69. package/src/registry/ui/loading-spinner.tsx +108 -111
  70. package/src/registry/ui/map.tsx +0 -0
  71. package/src/registry/ui/marquee.tsx +2 -0
  72. package/src/registry/ui/menubar.tsx +246 -246
  73. package/src/registry/ui/meta-pixel.tsx +46 -0
  74. package/src/registry/ui/microsoft-clarity.tsx +33 -0
  75. package/src/registry/ui/native-select.tsx +49 -52
  76. package/src/registry/ui/otp-input.tsx +152 -155
  77. package/src/registry/ui/pagination.tsx +149 -152
  78. package/src/registry/ui/patterns.tsx +28 -0
  79. package/src/registry/ui/popover.tsx +226 -227
  80. package/src/registry/ui/progress.tsx +51 -52
  81. package/src/registry/ui/radio.tsx +99 -102
  82. package/src/registry/ui/resizable.tsx +314 -314
  83. package/src/registry/ui/scroll-animation.tsx +45 -0
  84. package/src/registry/ui/scroll-area.tsx +121 -122
  85. package/src/registry/ui/scroll-to-top.tsx +0 -0
  86. package/src/registry/ui/search.tsx +147 -150
  87. package/src/registry/ui/select.tsx +292 -293
  88. package/src/registry/ui/separator.tsx +46 -47
  89. package/src/registry/ui/sheet.tsx +6 -3
  90. package/src/registry/ui/sidebar.tsx +628 -628
  91. package/src/registry/ui/skeleton.tsx +26 -29
  92. package/src/registry/ui/slider.tsx +196 -197
  93. package/src/registry/ui/slot.tsx +69 -72
  94. package/src/registry/ui/star-rating.tsx +131 -134
  95. package/src/registry/ui/switch.tsx +72 -73
  96. package/src/registry/ui/table-of-contents.tsx +96 -96
  97. package/src/registry/ui/table.tsx +138 -139
  98. package/src/registry/ui/tabs.tsx +124 -125
  99. package/src/registry/ui/text.tsx +61 -64
  100. package/src/registry/ui/textarea.tsx +41 -42
  101. package/src/registry/ui/theme-switcher.tsx +66 -66
  102. package/src/registry/ui/tiktok-pixel.tsx +36 -0
  103. package/src/registry/ui/toast.tsx +97 -98
  104. package/src/registry/ui/toggle-group.tsx +129 -129
  105. package/src/registry/ui/toggle.tsx +72 -72
  106. package/src/registry/ui/tooltip.tsx +143 -144
  107. package/src/registry/ui/whatsapp.tsx +0 -0
@@ -1,227 +1,226 @@
1
- "use client"
2
-
3
- import * as React from "react"
4
- import { createPortal } from "react-dom"
5
- import { cn } from "@/lib/utils"
6
- import { Slot } from "@/components/ui/slot"
7
-
8
- interface PopoverContextValue {
9
- open: boolean
10
- onOpenChange: (open: boolean) => void
11
- triggerRef: React.RefObject<HTMLButtonElement | null>
12
- }
13
-
14
- const PopoverContext = React.createContext<PopoverContextValue | null>(null)
15
-
16
- interface PopoverProps {
17
- children: React.ReactNode
18
- open?: boolean
19
- onOpenChange?: (open: boolean) => void
20
- defaultOpen?: boolean
21
- }
22
-
23
- /**
24
- * Popover component for floating content
25
- *
26
- * @example
27
- * <Popover>
28
- * <PopoverTrigger asChild>
29
- * <Button>Open Popover</Button>
30
- * </PopoverTrigger>
31
- * <PopoverContent>
32
- * Popover content here
33
- * </PopoverContent>
34
- * </Popover>
35
- */
36
- function Popover({ children, open: controlledOpen, onOpenChange, defaultOpen = false }: PopoverProps) {
37
- const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen)
38
- const triggerRef = React.useRef<HTMLButtonElement>(null)
39
-
40
- const open = controlledOpen !== undefined ? controlledOpen : uncontrolledOpen
41
- const setOpen = onOpenChange || setUncontrolledOpen
42
-
43
- return (
44
- <PopoverContext.Provider value={{ open, onOpenChange: setOpen, triggerRef }}>
45
- <div className="relative inline-block">
46
- {children}
47
- </div>
48
- </PopoverContext.Provider>
49
- )
50
- }
51
-
52
- interface PopoverTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
53
- asChild?: boolean
54
- }
55
-
56
- const PopoverTrigger = React.forwardRef<HTMLButtonElement, PopoverTriggerProps>(
57
- ({ onClick, asChild, children, ...props }, ref) => {
58
- const context = React.useContext(PopoverContext)
59
- if (!context) throw new Error("PopoverTrigger must be used within Popover")
60
-
61
- const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
62
- onClick?.(e)
63
- context.onOpenChange(!context.open)
64
- }
65
-
66
- // Combine refs
67
- const combinedRef = (node: HTMLButtonElement | null) => {
68
- (context.triggerRef as any).current = node
69
- if (typeof ref === 'function') ref(node)
70
- else if (ref) ref.current = node
71
- }
72
-
73
- const Comp = asChild ? Slot : "button"
74
-
75
- return (
76
- <Comp
77
- ref={combinedRef}
78
- aria-expanded={context.open}
79
- aria-haspopup={true}
80
- onClick={handleClick}
81
- {...props}
82
- >
83
- {children}
84
- </Comp>
85
- )
86
- }
87
- )
88
- PopoverTrigger.displayName = "PopoverTrigger"
89
-
90
- const PopoverContent = React.forwardRef<
91
- HTMLDivElement,
92
- React.HTMLAttributes<HTMLDivElement> & { align?: "start" | "center" | "end"; sideOffset?: number; portal?: boolean }
93
- >(({ className, children, align = "center", sideOffset = 4, portal = true, ...props }, ref) => {
94
- const context = React.useContext(PopoverContext)
95
- if (!context) throw new Error("PopoverContent must be used within Popover")
96
- const contentRef = React.useRef<HTMLDivElement>(null)
97
- const [position, setPosition] = React.useState({ top: 0, left: 0 })
98
-
99
- // Reset scroll on mount/unmount if needed, but mainly we just need a portal container
100
- const [mounted, setMounted] = React.useState(false)
101
- React.useEffect(() => {
102
- setMounted(true)
103
- }, [])
104
-
105
- React.useEffect(() => {
106
- const handleClickOutside = (e: MouseEvent) => {
107
- if (context.open) {
108
- const target = e.target as Node
109
- const content = contentRef.current
110
- const trigger = context.triggerRef.current
111
-
112
- // Don't close if clicking inside content or trigger
113
- if (content?.contains(target) || (trigger && trigger.contains(target))) {
114
- return
115
- }
116
- context.onOpenChange(false)
117
- }
118
- }
119
-
120
- const handleEscape = (e: KeyboardEvent) => {
121
- if (e.key === "Escape" && context.open) {
122
- context.onOpenChange(false)
123
- }
124
- }
125
-
126
- const checkPosition = () => {
127
- if (context.open && contentRef.current && context.triggerRef.current) {
128
- const triggerRect = context.triggerRef.current.getBoundingClientRect()
129
- const contentRect = contentRef.current.getBoundingClientRect()
130
- const viewportHeight = window.innerHeight
131
- const viewportWidth = window.innerWidth
132
-
133
- let top = 0
134
- let left = 0
135
-
136
- // Vertical
137
- const spaceBelow = viewportHeight - triggerRect.bottom
138
- const spaceAbove = triggerRect.top
139
- const neededHeight = contentRect.height + sideOffset
140
- const onBottom = spaceBelow >= neededHeight || spaceBelow > spaceAbove
141
-
142
- if (onBottom) {
143
- top = triggerRect.bottom + sideOffset
144
- } else {
145
- top = triggerRect.top - contentRect.height - sideOffset
146
- }
147
-
148
- // Horizontal (Alignment)
149
- if (align === 'start') {
150
- left = triggerRect.left
151
- } else if (align === 'end') {
152
- left = triggerRect.right - contentRect.width
153
- } else {
154
- // center
155
- left = triggerRect.left + (triggerRect.width - contentRect.width) / 2
156
- }
157
-
158
- // Clamping
159
- if (left < 4) left = 4
160
- if (left + contentRect.width > viewportWidth - 4) {
161
- left = viewportWidth - contentRect.width - 4
162
- }
163
-
164
- setPosition({ top, left })
165
- }
166
- }
167
-
168
- if (context.open) {
169
- requestAnimationFrame(checkPosition)
170
- }
171
-
172
- const timer = setTimeout(() => {
173
- document.addEventListener("click", handleClickOutside)
174
- }, 0)
175
- document.addEventListener("keydown", handleEscape)
176
- window.addEventListener("resize", checkPosition)
177
- window.addEventListener("scroll", checkPosition, true)
178
-
179
- return () => {
180
- clearTimeout(timer)
181
- document.removeEventListener("click", handleClickOutside)
182
- document.removeEventListener("keydown", handleEscape)
183
- window.removeEventListener("resize", checkPosition)
184
- window.removeEventListener("scroll", checkPosition, true)
185
- }
186
- }, [context.open, context, align, sideOffset])
187
-
188
- if (!context.open) return null
189
- if (portal && !mounted) return null
190
-
191
- const content = (
192
- <div
193
- ref={(node) => {
194
- (contentRef as any).current = node
195
- if (typeof ref === 'function') ref(node)
196
- else if (ref) ref.current = node
197
- }}
198
- className={cn(
199
- "z-50 min-w-[10rem] rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none",
200
- "animate-in fade-in-0 zoom-in-95",
201
- !portal && "absolute",
202
- !portal && "mt-2",
203
- portal && "fixed",
204
- className
205
- )}
206
- style={{
207
- top: portal ? position.top : undefined,
208
- left: portal ? position.left : undefined,
209
- ...props.style
210
- }}
211
- onClick={(e) => e.stopPropagation()}
212
- {...props}
213
- >
214
- {children}
215
- </div>
216
- )
217
-
218
- if (portal) {
219
- return createPortal(content, document.body)
220
- }
221
-
222
- return content
223
- })
224
- PopoverContent.displayName = "PopoverContent"
225
-
226
- export { Popover, PopoverTrigger, PopoverContent }
227
-
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { createPortal } from "react-dom"
5
+ import { cn } from "@/lib/utils"
6
+ import { Slot } from "@/components/ui/slot"
7
+
8
+ interface PopoverContextValue {
9
+ open: boolean
10
+ onOpenChange: (open: boolean) => void
11
+ triggerRef: React.RefObject<HTMLButtonElement | null>
12
+ }
13
+
14
+ const PopoverContext = React.createContext<PopoverContextValue | null>(null)
15
+
16
+ interface PopoverProps {
17
+ children: React.ReactNode
18
+ open?: boolean
19
+ onOpenChange?: (open: boolean) => void
20
+ defaultOpen?: boolean
21
+ }
22
+
23
+ /**
24
+ * Popover component for floating content
25
+ *
26
+ * @example
27
+ * <Popover>
28
+ * <PopoverTrigger asChild>
29
+ * <Button>Open Popover</Button>
30
+ * </PopoverTrigger>
31
+ * <PopoverContent>
32
+ * Popover content here
33
+ * </PopoverContent>
34
+ * </Popover>
35
+ */
36
+ function Popover({ children, open: controlledOpen, onOpenChange, defaultOpen = false }: PopoverProps) {
37
+ const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen)
38
+ const triggerRef = React.useRef<HTMLButtonElement>(null)
39
+
40
+ const open = controlledOpen !== undefined ? controlledOpen : uncontrolledOpen
41
+ const setOpen = onOpenChange || setUncontrolledOpen
42
+
43
+ return (
44
+ <PopoverContext.Provider value={{ open, onOpenChange: setOpen, triggerRef }}>
45
+ <div className="relative inline-block">
46
+ {children}
47
+ </div>
48
+ </PopoverContext.Provider>
49
+ )
50
+ }
51
+
52
+ interface PopoverTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
53
+ asChild?: boolean
54
+ }
55
+
56
+ const PopoverTrigger = React.forwardRef<HTMLButtonElement, PopoverTriggerProps>(
57
+ ({ onClick, asChild, children, ...props }, ref) => {
58
+ const context = React.useContext(PopoverContext)
59
+ if (!context) throw new Error("PopoverTrigger must be used within Popover")
60
+
61
+ const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
62
+ onClick?.(e)
63
+ context.onOpenChange(!context.open)
64
+ }
65
+
66
+ // Combine refs
67
+ const combinedRef = (node: HTMLButtonElement | null) => {
68
+ (context.triggerRef as any).current = node
69
+ if (typeof ref === 'function') ref(node)
70
+ else if (ref) ref.current = node
71
+ }
72
+
73
+ const Comp = asChild ? Slot : "button"
74
+
75
+ return (
76
+ <Comp
77
+ ref={combinedRef}
78
+ aria-expanded={context.open}
79
+ aria-haspopup={true}
80
+ onClick={handleClick}
81
+ {...props}
82
+ >
83
+ {children}
84
+ </Comp>
85
+ )
86
+ }
87
+ )
88
+ PopoverTrigger.displayName = "PopoverTrigger"
89
+
90
+ const PopoverContent = React.forwardRef<
91
+ HTMLDivElement,
92
+ React.HTMLAttributes<HTMLDivElement> & { align?: "start" | "center" | "end"; sideOffset?: number; portal?: boolean }
93
+ >(({ className, children, align = "center", sideOffset = 4, portal = true, ...props }, ref) => {
94
+ const context = React.useContext(PopoverContext)
95
+ if (!context) throw new Error("PopoverContent must be used within Popover")
96
+ const contentRef = React.useRef<HTMLDivElement>(null)
97
+ const [position, setPosition] = React.useState({ top: 0, left: 0 })
98
+
99
+ // Reset scroll on mount/unmount if needed, but mainly we just need a portal container
100
+ const [mounted, setMounted] = React.useState(false)
101
+ React.useEffect(() => {
102
+ setMounted(true)
103
+ }, [])
104
+
105
+ React.useEffect(() => {
106
+ const handleClickOutside = (e: MouseEvent) => {
107
+ if (context.open) {
108
+ const target = e.target as Node
109
+ const content = contentRef.current
110
+ const trigger = context.triggerRef.current
111
+
112
+ // Don't close if clicking inside content or trigger
113
+ if (content?.contains(target) || (trigger && trigger.contains(target))) {
114
+ return
115
+ }
116
+ context.onOpenChange(false)
117
+ }
118
+ }
119
+
120
+ const handleEscape = (e: KeyboardEvent) => {
121
+ if (e.key === "Escape" && context.open) {
122
+ context.onOpenChange(false)
123
+ }
124
+ }
125
+
126
+ const checkPosition = () => {
127
+ if (context.open && contentRef.current && context.triggerRef.current) {
128
+ const triggerRect = context.triggerRef.current.getBoundingClientRect()
129
+ const contentRect = contentRef.current.getBoundingClientRect()
130
+ const viewportHeight = window.innerHeight
131
+ const viewportWidth = window.innerWidth
132
+
133
+ let top = 0
134
+ let left = 0
135
+
136
+ // Vertical
137
+ const spaceBelow = viewportHeight - triggerRect.bottom
138
+ const spaceAbove = triggerRect.top
139
+ const neededHeight = contentRect.height + sideOffset
140
+ const onBottom = spaceBelow >= neededHeight || spaceBelow > spaceAbove
141
+
142
+ if (onBottom) {
143
+ top = triggerRect.bottom + sideOffset
144
+ } else {
145
+ top = triggerRect.top - contentRect.height - sideOffset
146
+ }
147
+
148
+ // Horizontal (Alignment)
149
+ if (align === 'start') {
150
+ left = triggerRect.left
151
+ } else if (align === 'end') {
152
+ left = triggerRect.right - contentRect.width
153
+ } else {
154
+ // center
155
+ left = triggerRect.left + (triggerRect.width - contentRect.width) / 2
156
+ }
157
+
158
+ // Clamping
159
+ if (left < 4) left = 4
160
+ if (left + contentRect.width > viewportWidth - 4) {
161
+ left = viewportWidth - contentRect.width - 4
162
+ }
163
+
164
+ setPosition({ top, left })
165
+ }
166
+ }
167
+
168
+ if (context.open) {
169
+ requestAnimationFrame(checkPosition)
170
+ }
171
+
172
+ const timer = setTimeout(() => {
173
+ document.addEventListener("click", handleClickOutside)
174
+ }, 0)
175
+ document.addEventListener("keydown", handleEscape)
176
+ window.addEventListener("resize", checkPosition)
177
+ window.addEventListener("scroll", checkPosition, true)
178
+
179
+ return () => {
180
+ clearTimeout(timer)
181
+ document.removeEventListener("click", handleClickOutside)
182
+ document.removeEventListener("keydown", handleEscape)
183
+ window.removeEventListener("resize", checkPosition)
184
+ window.removeEventListener("scroll", checkPosition, true)
185
+ }
186
+ }, [context.open, context, align, sideOffset])
187
+
188
+ if (!context.open) return null
189
+ if (portal && !mounted) return null
190
+
191
+ const content = (
192
+ <div
193
+ ref={(node) => {
194
+ (contentRef as any).current = node
195
+ if (typeof ref === 'function') ref(node)
196
+ else if (ref) ref.current = node
197
+ }}
198
+ className={cn(
199
+ "z-50 min-w-[10rem] rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none",
200
+ "animate-in fade-in-0 zoom-in-95",
201
+ !portal && "absolute",
202
+ !portal && "mt-2",
203
+ portal && "fixed",
204
+ className
205
+ )}
206
+ style={{
207
+ top: portal ? position.top : undefined,
208
+ left: portal ? position.left : undefined,
209
+ ...props.style
210
+ }}
211
+ onClick={(e) => e.stopPropagation()}
212
+ {...props}
213
+ >
214
+ {children}
215
+ </div>
216
+ )
217
+
218
+ if (portal) {
219
+ return createPortal(content, document.body)
220
+ }
221
+
222
+ return content
223
+ })
224
+ PopoverContent.displayName = "PopoverContent"
225
+
226
+ export { Popover, PopoverTrigger, PopoverContent }
@@ -1,52 +1,51 @@
1
- "use client"
2
-
3
- import * as React from "react"
4
- import { cn } from "@/lib/utils"
5
-
6
- interface ProgressProps extends React.HTMLAttributes<HTMLDivElement> {
7
- /**
8
- * Progress value from 0 to 100
9
- */
10
- value?: number
11
- /**
12
- * Maximum value
13
- * @default 100
14
- */
15
- max?: number
16
- }
17
-
18
- /**
19
- * Progress bar component
20
- *
21
- * @example
22
- * <Progress value={60} />
23
- */
24
- const Progress = React.forwardRef<HTMLDivElement, ProgressProps>(
25
- ({ className, value = 0, max = 100, ...props }, ref) => {
26
- const percentage = Math.min(Math.max((value / max) * 100, 0), 100)
27
-
28
- return (
29
- <div
30
- ref={ref}
31
- role="progressbar"
32
- aria-valuenow={value}
33
- aria-valuemin={0}
34
- aria-valuemax={max}
35
- className={cn(
36
- "relative h-2 w-full overflow-hidden rounded-full bg-primary/20",
37
- className
38
- )}
39
- {...props}
40
- >
41
- <div
42
- className="h-full w-full flex-1 bg-primary transition-all"
43
- style={{ transform: `translateX(-${100 - percentage}%)` }}
44
- />
45
- </div>
46
- )
47
- }
48
- )
49
- Progress.displayName = "Progress"
50
-
51
- export { Progress }
52
-
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "@/lib/utils"
5
+
6
+ interface ProgressProps extends React.HTMLAttributes<HTMLDivElement> {
7
+ /**
8
+ * Progress value from 0 to 100
9
+ */
10
+ value?: number
11
+ /**
12
+ * Maximum value
13
+ * @default 100
14
+ */
15
+ max?: number
16
+ }
17
+
18
+ /**
19
+ * Progress bar component
20
+ *
21
+ * @example
22
+ * <Progress value={60} />
23
+ */
24
+ const Progress = React.forwardRef<HTMLDivElement, ProgressProps>(
25
+ ({ className, value = 0, max = 100, ...props }, ref) => {
26
+ const percentage = Math.min(Math.max((value / max) * 100, 0), 100)
27
+
28
+ return (
29
+ <div
30
+ ref={ref}
31
+ role="progressbar"
32
+ aria-valuenow={value}
33
+ aria-valuemin={0}
34
+ aria-valuemax={max}
35
+ className={cn(
36
+ "relative h-2 w-full overflow-hidden rounded-full bg-primary/20",
37
+ className
38
+ )}
39
+ {...props}
40
+ >
41
+ <div
42
+ className="h-full w-full flex-1 bg-primary transition-all"
43
+ style={{ transform: `translateX(-${100 - percentage}%)` }}
44
+ />
45
+ </div>
46
+ )
47
+ }
48
+ )
49
+ Progress.displayName = "Progress"
50
+
51
+ export { Progress }