@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,91 +1,88 @@
1
- "use client"
2
-
3
- import * as React from "react"
4
- import { cn } from "@/lib/utils"
5
-
6
- /**
7
- * Card component - Container with header, content, and footer
8
- *
9
- * @example
10
- * <Card>
11
- * <CardHeader>
12
- * <CardTitle>Title</CardTitle>
13
- * <CardDescription>Description</CardDescription>
14
- * </CardHeader>
15
- * <CardContent>Content goes here</CardContent>
16
- * <CardFooter>Footer actions</CardFooter>
17
- * </Card>
18
- */
19
-
20
- const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
21
- ({ className, ...props }, ref) => (
22
- <div
23
- ref={ref}
24
- className={cn(
25
- "rounded-xl border bg-card text-card-foreground shadow",
26
- className
27
- )}
28
- {...props}
29
- />
30
- )
31
- )
32
- Card.displayName = "Card"
33
-
34
- const CardHeader = React.forwardRef<
35
- HTMLDivElement,
36
- React.HTMLAttributes<HTMLDivElement>
37
- >(({ className, ...props }, ref) => (
38
- <div
39
- ref={ref}
40
- className={cn("flex flex-col space-y-1.5 p-6", className)}
41
- {...props}
42
- />
43
- ))
44
- CardHeader.displayName = "CardHeader"
45
-
46
- const CardTitle = React.forwardRef<
47
- HTMLHeadingElement,
48
- React.HTMLAttributes<HTMLHeadingElement>
49
- >(({ className, ...props }, ref) => (
50
- <h3
51
- ref={ref}
52
- className={cn("font-semibold leading-none tracking-tight", className)}
53
- {...props}
54
- />
55
- ))
56
- CardTitle.displayName = "CardTitle"
57
-
58
- const CardDescription = React.forwardRef<
59
- HTMLParagraphElement,
60
- React.HTMLAttributes<HTMLParagraphElement>
61
- >(({ className, ...props }, ref) => (
62
- <p
63
- ref={ref}
64
- className={cn("text-sm text-muted-foreground", className)}
65
- {...props}
66
- />
67
- ))
68
- CardDescription.displayName = "CardDescription"
69
-
70
- const CardContent = React.forwardRef<
71
- HTMLDivElement,
72
- React.HTMLAttributes<HTMLDivElement>
73
- >(({ className, ...props }, ref) => (
74
- <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
75
- ))
76
- CardContent.displayName = "CardContent"
77
-
78
- const CardFooter = React.forwardRef<
79
- HTMLDivElement,
80
- React.HTMLAttributes<HTMLDivElement>
81
- >(({ className, ...props }, ref) => (
82
- <div
83
- ref={ref}
84
- className={cn("flex items-center p-6 pt-0", className)}
85
- {...props}
86
- />
87
- ))
88
- CardFooter.displayName = "CardFooter"
89
-
90
- export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
91
-
1
+ import * as React from "react"
2
+ import { cn } from "@/lib/utils"
3
+
4
+ /**
5
+ * Card component - Container with header, content, and footer
6
+ *
7
+ * @example
8
+ * <Card>
9
+ * <CardHeader>
10
+ * <CardTitle>Title</CardTitle>
11
+ * <CardDescription>Description</CardDescription>
12
+ * </CardHeader>
13
+ * <CardContent>Content goes here</CardContent>
14
+ * <CardFooter>Footer actions</CardFooter>
15
+ * </Card>
16
+ */
17
+
18
+ const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
19
+ ({ className, ...props }, ref) => (
20
+ <div
21
+ ref={ref}
22
+ className={cn(
23
+ "rounded-xl border bg-card text-card-foreground shadow",
24
+ className
25
+ )}
26
+ {...props}
27
+ />
28
+ )
29
+ )
30
+ Card.displayName = "Card"
31
+
32
+ const CardHeader = React.forwardRef<
33
+ HTMLDivElement,
34
+ React.HTMLAttributes<HTMLDivElement>
35
+ >(({ className, ...props }, ref) => (
36
+ <div
37
+ ref={ref}
38
+ className={cn("flex flex-col space-y-1.5 p-6", className)}
39
+ {...props}
40
+ />
41
+ ))
42
+ CardHeader.displayName = "CardHeader"
43
+
44
+ const CardTitle = React.forwardRef<
45
+ HTMLHeadingElement,
46
+ React.HTMLAttributes<HTMLHeadingElement>
47
+ >(({ className, ...props }, ref) => (
48
+ <h3
49
+ ref={ref}
50
+ className={cn("font-semibold leading-none tracking-tight", className)}
51
+ {...props}
52
+ />
53
+ ))
54
+ CardTitle.displayName = "CardTitle"
55
+
56
+ const CardDescription = React.forwardRef<
57
+ HTMLParagraphElement,
58
+ React.HTMLAttributes<HTMLParagraphElement>
59
+ >(({ className, ...props }, ref) => (
60
+ <p
61
+ ref={ref}
62
+ className={cn("text-sm text-muted-foreground", className)}
63
+ {...props}
64
+ />
65
+ ))
66
+ CardDescription.displayName = "CardDescription"
67
+
68
+ const CardContent = React.forwardRef<
69
+ HTMLDivElement,
70
+ React.HTMLAttributes<HTMLDivElement>
71
+ >(({ className, ...props }, ref) => (
72
+ <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
73
+ ))
74
+ CardContent.displayName = "CardContent"
75
+
76
+ const CardFooter = React.forwardRef<
77
+ HTMLDivElement,
78
+ React.HTMLAttributes<HTMLDivElement>
79
+ >(({ className, ...props }, ref) => (
80
+ <div
81
+ ref={ref}
82
+ className={cn("flex items-center p-6 pt-0", className)}
83
+ {...props}
84
+ />
85
+ ))
86
+ CardFooter.displayName = "CardFooter"
87
+
88
+ export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
@@ -1,214 +1,214 @@
1
- 'use client'
2
- import * as React from "react"
3
- import { cn } from "@/lib/utils"
4
-
5
- interface CarouselContextValue {
6
- currentIndex: number
7
- setCurrentIndex: React.Dispatch<React.SetStateAction<number>>
8
- itemsCount: number
9
- setItemsCount: React.Dispatch<React.SetStateAction<number>>
10
- isTransitioning: boolean
11
- setIsTransitioning: React.Dispatch<React.SetStateAction<boolean>>
12
- }
13
-
14
- const CarouselContext = React.createContext<CarouselContextValue | null>(null)
15
-
16
- interface CarouselProps extends React.HTMLAttributes<HTMLDivElement> {
17
- /** Auto-play interval in ms (0 to disable) */
18
- autoPlay?: number
19
- /** Loop back to start (ignored in this infinite implementation as it's always true-ish, but kept for API) */
20
- loop?: boolean
21
- }
22
-
23
- /**
24
- * Carousel/Slider component with Infinite Looping
25
- */
26
- const Carousel = React.forwardRef<HTMLDivElement, CarouselProps>(
27
- ({ className, children, autoPlay = 0, loop = true, ...props }, ref) => {
28
- // Start at 1 because 0 is the clone of the last item
29
- const [currentIndex, setCurrentIndex] = React.useState(1)
30
- const [itemsCount, setItemsCount] = React.useState(0)
31
- const [isTransitioning, setIsTransitioning] = React.useState(true)
32
- const intervalRef = React.useRef<NodeJS.Timeout | null>(null)
33
-
34
- React.useEffect(() => {
35
- if (autoPlay > 0 && itemsCount > 1) {
36
- intervalRef.current = setInterval(() => {
37
- setIsTransitioning(true)
38
- setCurrentIndex((prev) => prev + 1)
39
- }, autoPlay)
40
- return () => {
41
- if (intervalRef.current) clearInterval(intervalRef.current)
42
- }
43
- }
44
- }, [autoPlay, itemsCount])
45
-
46
- return (
47
- <CarouselContext.Provider value={{
48
- currentIndex,
49
- setCurrentIndex,
50
- itemsCount,
51
- setItemsCount,
52
- isTransitioning,
53
- setIsTransitioning
54
- }}>
55
- <div
56
- ref={ref}
57
- className={cn("relative", className)}
58
- role="region"
59
- aria-roledescription="carousel"
60
- {...props}
61
- >
62
- {children}
63
- </div>
64
- </CarouselContext.Provider>
65
- )
66
- }
67
- )
68
- Carousel.displayName = "Carousel"
69
-
70
- const CarouselContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
71
- ({ className, children, ...props }, ref) => {
72
- const context = React.useContext(CarouselContext)
73
- if (!context) throw new Error("CarouselContent must be used within Carousel")
74
-
75
- const items = React.Children.toArray(children)
76
-
77
- React.useEffect(() => {
78
- context.setItemsCount(items.length)
79
- }, [items.length, context])
80
-
81
- // Clone first and last items for infinite loop illusion
82
- const firstClone = items.length > 0 && React.isValidElement(items[0])
83
- ? React.cloneElement(items[0] as React.ReactElement, { key: "clone-first" })
84
- : null
85
- const lastClone = items.length > 0 && React.isValidElement(items[items.length - 1])
86
- ? React.cloneElement(items[items.length - 1] as React.ReactElement, { key: "clone-last" })
87
- : null
88
-
89
- // If we have items, prepend last-clone and append first-clone
90
- const displayItems = items.length > 1 ? [lastClone, ...items, firstClone] : items
91
-
92
- const handleTransitionEnd = () => {
93
- if (items.length <= 1) return
94
-
95
- // If reached the end clone (index = N + 1), snap back to first real item (index = 1)
96
- if (context.currentIndex >= items.length + 1) {
97
- context.setIsTransitioning(false)
98
- context.setCurrentIndex(1)
99
- }
100
- // If reached the start clone (index = 0), snap forward to last real item (index = N)
101
- else if (context.currentIndex <= 0) {
102
- context.setIsTransitioning(false)
103
- context.setCurrentIndex(items.length)
104
- }
105
- }
106
-
107
- // Re-enable transition after a snap (on next frame)
108
- React.useEffect(() => {
109
- if (!context.isTransitioning) {
110
- const timer = setTimeout(() => {
111
- context.setIsTransitioning(true)
112
- }, 50)
113
- return () => clearTimeout(timer)
114
- }
115
- }, [context.isTransitioning, context])
116
-
117
- return (
118
- <div ref={ref} className={cn("overflow-hidden", className)} {...props}>
119
- <div
120
- className="flex h-full w-full"
121
- style={{
122
- transform: `translateX(-${context.currentIndex * 100}%)`,
123
- transition: context.isTransitioning ? 'transform 300ms ease-in-out' : 'none'
124
- }}
125
- onTransitionEnd={handleTransitionEnd}
126
- >
127
- {displayItems}
128
- </div>
129
- </div>
130
- )
131
- }
132
- )
133
- CarouselContent.displayName = "CarouselContent"
134
-
135
- const CarouselItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
136
- ({ className, ...props }, ref) => (
137
- <div
138
- ref={ref}
139
- role="group"
140
- aria-roledescription="slide"
141
- className={cn("min-w-0 h-full shrink-0 grow-0 basis-full", className)}
142
- {...props}
143
- />
144
- )
145
- )
146
- CarouselItem.displayName = "CarouselItem"
147
-
148
- const CarouselPrevious = React.forwardRef<
149
- HTMLButtonElement,
150
- React.ButtonHTMLAttributes<HTMLButtonElement>
151
- >(({ className, ...props }, ref) => {
152
- const context = React.useContext(CarouselContext)
153
- if (!context) throw new Error("CarouselPrevious must be used within Carousel")
154
-
155
- const handlePrev = () => {
156
- context.setIsTransitioning(true)
157
- context.setCurrentIndex((prev) => prev - 1)
158
- }
159
-
160
- return (
161
- <button
162
- ref={ref}
163
- type="button"
164
- className={cn(
165
- "absolute left-4 top-1/2 -translate-y-1/2 h-8 w-8 rounded-full border bg-background shadow-md flex items-center justify-center cursor-pointer",
166
- "hover:bg-accent disabled:opacity-50",
167
- className
168
- )}
169
- onClick={handlePrev}
170
- aria-label="Previous slide"
171
- {...props}
172
- >
173
- <svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
174
- <path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
175
- </svg>
176
- </button>
177
- )
178
- })
179
- CarouselPrevious.displayName = "CarouselPrevious"
180
-
181
- const CarouselNext = React.forwardRef<
182
- HTMLButtonElement,
183
- React.ButtonHTMLAttributes<HTMLButtonElement>
184
- >(({ className, ...props }, ref) => {
185
- const context = React.useContext(CarouselContext)
186
- if (!context) throw new Error("CarouselNext must be used within Carousel")
187
-
188
- const handleNext = () => {
189
- context.setIsTransitioning(true)
190
- context.setCurrentIndex((prev) => prev + 1)
191
- }
192
-
193
- return (
194
- <button
195
- ref={ref}
196
- type="button"
197
- className={cn(
198
- "absolute right-4 top-1/2 -translate-y-1/2 h-8 w-8 rounded-full border bg-background shadow-md flex items-center justify-center cursor-pointer",
199
- "hover:bg-accent disabled:opacity-50",
200
- className
201
- )}
202
- onClick={handleNext}
203
- aria-label="Next slide"
204
- {...props}
205
- >
206
- <svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
207
- <path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
208
- </svg>
209
- </button>
210
- )
211
- })
212
- CarouselNext.displayName = "CarouselNext"
213
-
214
- export { Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext }
1
+ 'use client'
2
+ import * as React from "react"
3
+ import { cn } from "@/lib/utils"
4
+
5
+ interface CarouselContextValue {
6
+ currentIndex: number
7
+ setCurrentIndex: React.Dispatch<React.SetStateAction<number>>
8
+ itemsCount: number
9
+ setItemsCount: React.Dispatch<React.SetStateAction<number>>
10
+ isTransitioning: boolean
11
+ setIsTransitioning: React.Dispatch<React.SetStateAction<boolean>>
12
+ }
13
+
14
+ const CarouselContext = React.createContext<CarouselContextValue | null>(null)
15
+
16
+ interface CarouselProps extends React.HTMLAttributes<HTMLDivElement> {
17
+ /** Auto-play interval in ms (0 to disable) */
18
+ autoPlay?: number
19
+ /** Loop back to start (ignored in this infinite implementation as it's always true-ish, but kept for API) */
20
+ loop?: boolean
21
+ }
22
+
23
+ /**
24
+ * Carousel/Slider component with Infinite Looping
25
+ */
26
+ const Carousel = React.forwardRef<HTMLDivElement, CarouselProps>(
27
+ ({ className, children, autoPlay = 0, loop = true, ...props }, ref) => {
28
+ // Start at 1 because 0 is the clone of the last item
29
+ const [currentIndex, setCurrentIndex] = React.useState(1)
30
+ const [itemsCount, setItemsCount] = React.useState(0)
31
+ const [isTransitioning, setIsTransitioning] = React.useState(true)
32
+ const intervalRef = React.useRef<NodeJS.Timeout | null>(null)
33
+
34
+ React.useEffect(() => {
35
+ if (autoPlay > 0 && itemsCount > 1) {
36
+ intervalRef.current = setInterval(() => {
37
+ setIsTransitioning(true)
38
+ setCurrentIndex((prev) => prev + 1)
39
+ }, autoPlay)
40
+ return () => {
41
+ if (intervalRef.current) clearInterval(intervalRef.current)
42
+ }
43
+ }
44
+ }, [autoPlay, itemsCount])
45
+
46
+ return (
47
+ <CarouselContext.Provider value={{
48
+ currentIndex,
49
+ setCurrentIndex,
50
+ itemsCount,
51
+ setItemsCount,
52
+ isTransitioning,
53
+ setIsTransitioning
54
+ }}>
55
+ <div
56
+ ref={ref}
57
+ className={cn("relative", className)}
58
+ role="region"
59
+ aria-roledescription="carousel"
60
+ {...props}
61
+ >
62
+ {children}
63
+ </div>
64
+ </CarouselContext.Provider>
65
+ )
66
+ }
67
+ )
68
+ Carousel.displayName = "Carousel"
69
+
70
+ const CarouselContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
71
+ ({ className, children, ...props }, ref) => {
72
+ const context = React.useContext(CarouselContext)
73
+ if (!context) throw new Error("CarouselContent must be used within Carousel")
74
+
75
+ const items = React.Children.toArray(children)
76
+
77
+ React.useEffect(() => {
78
+ context.setItemsCount(items.length)
79
+ }, [items.length, context])
80
+
81
+ // Clone first and last items for infinite loop illusion
82
+ const firstClone = items.length > 0 && React.isValidElement(items[0])
83
+ ? React.cloneElement(items[0] as React.ReactElement, { key: "clone-first" })
84
+ : null
85
+ const lastClone = items.length > 0 && React.isValidElement(items[items.length - 1])
86
+ ? React.cloneElement(items[items.length - 1] as React.ReactElement, { key: "clone-last" })
87
+ : null
88
+
89
+ // If we have items, prepend last-clone and append first-clone
90
+ const displayItems = items.length > 1 ? [lastClone, ...items, firstClone] : items
91
+
92
+ const handleTransitionEnd = () => {
93
+ if (items.length <= 1) return
94
+
95
+ // If reached the end clone (index = N + 1), snap back to first real item (index = 1)
96
+ if (context.currentIndex >= items.length + 1) {
97
+ context.setIsTransitioning(false)
98
+ context.setCurrentIndex(1)
99
+ }
100
+ // If reached the start clone (index = 0), snap forward to last real item (index = N)
101
+ else if (context.currentIndex <= 0) {
102
+ context.setIsTransitioning(false)
103
+ context.setCurrentIndex(items.length)
104
+ }
105
+ }
106
+
107
+ // Re-enable transition after a snap (on next frame)
108
+ React.useEffect(() => {
109
+ if (!context.isTransitioning) {
110
+ const timer = setTimeout(() => {
111
+ context.setIsTransitioning(true)
112
+ }, 50)
113
+ return () => clearTimeout(timer)
114
+ }
115
+ }, [context.isTransitioning, context])
116
+
117
+ return (
118
+ <div ref={ref} className={cn("overflow-hidden", className)} {...props}>
119
+ <div
120
+ className="flex h-full w-full"
121
+ style={{
122
+ transform: `translateX(-${context.currentIndex * 100}%)`,
123
+ transition: context.isTransitioning ? 'transform 300ms ease-in-out' : 'none'
124
+ }}
125
+ onTransitionEnd={handleTransitionEnd}
126
+ >
127
+ {displayItems}
128
+ </div>
129
+ </div>
130
+ )
131
+ }
132
+ )
133
+ CarouselContent.displayName = "CarouselContent"
134
+
135
+ const CarouselItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
136
+ ({ className, ...props }, ref) => (
137
+ <div
138
+ ref={ref}
139
+ role="group"
140
+ aria-roledescription="slide"
141
+ className={cn("min-w-0 h-full shrink-0 grow-0 basis-full", className)}
142
+ {...props}
143
+ />
144
+ )
145
+ )
146
+ CarouselItem.displayName = "CarouselItem"
147
+
148
+ const CarouselPrevious = React.forwardRef<
149
+ HTMLButtonElement,
150
+ React.ButtonHTMLAttributes<HTMLButtonElement>
151
+ >(({ className, ...props }, ref) => {
152
+ const context = React.useContext(CarouselContext)
153
+ if (!context) throw new Error("CarouselPrevious must be used within Carousel")
154
+
155
+ const handlePrev = () => {
156
+ context.setIsTransitioning(true)
157
+ context.setCurrentIndex((prev) => prev - 1)
158
+ }
159
+
160
+ return (
161
+ <button
162
+ ref={ref}
163
+ type="button"
164
+ className={cn(
165
+ "absolute left-4 top-1/2 -translate-y-1/2 h-8 w-8 rounded-full border bg-background shadow-md flex items-center justify-center cursor-pointer",
166
+ "hover:bg-accent disabled:opacity-50",
167
+ className
168
+ )}
169
+ onClick={handlePrev}
170
+ aria-label="Previous slide"
171
+ {...props}
172
+ >
173
+ <svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
174
+ <path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
175
+ </svg>
176
+ </button>
177
+ )
178
+ })
179
+ CarouselPrevious.displayName = "CarouselPrevious"
180
+
181
+ const CarouselNext = React.forwardRef<
182
+ HTMLButtonElement,
183
+ React.ButtonHTMLAttributes<HTMLButtonElement>
184
+ >(({ className, ...props }, ref) => {
185
+ const context = React.useContext(CarouselContext)
186
+ if (!context) throw new Error("CarouselNext must be used within Carousel")
187
+
188
+ const handleNext = () => {
189
+ context.setIsTransitioning(true)
190
+ context.setCurrentIndex((prev) => prev + 1)
191
+ }
192
+
193
+ return (
194
+ <button
195
+ ref={ref}
196
+ type="button"
197
+ className={cn(
198
+ "absolute right-4 top-1/2 -translate-y-1/2 h-8 w-8 rounded-full border bg-background shadow-md flex items-center justify-center cursor-pointer",
199
+ "hover:bg-accent disabled:opacity-50",
200
+ className
201
+ )}
202
+ onClick={handleNext}
203
+ aria-label="Next slide"
204
+ {...props}
205
+ >
206
+ <svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
207
+ <path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
208
+ </svg>
209
+ </button>
210
+ )
211
+ })
212
+ CarouselNext.displayName = "CarouselNext"
213
+
214
+ export { Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext }