@srcroot/ui 0.0.28 → 0.0.32

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 (77) hide show
  1. package/dist/index.js +377 -574
  2. package/package.json +2 -2
  3. package/src/registry/analytics/google-analytics.tsx +36 -0
  4. package/src/registry/analytics/google-tag-manager.tsx +62 -0
  5. package/src/registry/analytics/meta-pixel.tsx +44 -0
  6. package/src/registry/analytics/microsoft-clarity.tsx +31 -0
  7. package/src/registry/analytics/tiktok-pixel.tsx +34 -0
  8. package/src/registry/lib/utils.ts +6 -0
  9. package/src/registry/themes/gradients.css +236 -0
  10. package/{registry/themes → src/registry/themes/v3}/gray.css +45 -1
  11. package/{registry/themes → src/registry/themes/v3}/neutral.css +45 -1
  12. package/{registry/themes → src/registry/themes/v3}/slate.css +45 -1
  13. package/{registry/themes → src/registry/themes/v3}/stone.css +45 -1
  14. package/{registry/themes → src/registry/themes/v3}/zinc.css +45 -1
  15. package/src/registry/themes/v4/gray.css +183 -0
  16. package/src/registry/themes/v4/neutral.css +183 -0
  17. package/src/registry/themes/v4/slate.css +183 -0
  18. package/src/registry/themes/v4/stone.css +183 -0
  19. package/src/registry/themes/v4/zinc.css +183 -0
  20. package/{registry → src/registry/ui}/carousel.tsx +82 -38
  21. package/src/registry/ui/chatbot.tsx +96 -0
  22. package/registry/design-tokens.css +0 -217
  23. package/registry/themes/index.css +0 -19
  24. /package/{registry → src/registry/ui}/accordion.tsx +0 -0
  25. /package/{registry → src/registry/ui}/alert-dialog.tsx +0 -0
  26. /package/{registry → src/registry/ui}/alert.tsx +0 -0
  27. /package/{registry → src/registry/ui}/aspect-ratio.tsx +0 -0
  28. /package/{registry → src/registry/ui}/avatar.tsx +0 -0
  29. /package/{registry → src/registry/ui}/badge.tsx +0 -0
  30. /package/{registry → src/registry/ui}/breadcrumb.tsx +0 -0
  31. /package/{registry → src/registry/ui}/button-group.tsx +0 -0
  32. /package/{registry → src/registry/ui}/button.tsx +0 -0
  33. /package/{registry → src/registry/ui}/calendar.tsx +0 -0
  34. /package/{registry → src/registry/ui}/card.tsx +0 -0
  35. /package/{registry → src/registry/ui}/checkbox.tsx +0 -0
  36. /package/{registry → src/registry/ui}/collapsible.tsx +0 -0
  37. /package/{registry → src/registry/ui}/combobox.tsx +0 -0
  38. /package/{registry → src/registry/ui}/command.tsx +0 -0
  39. /package/{registry → src/registry/ui}/container.tsx +0 -0
  40. /package/{registry → src/registry/ui}/context-menu.tsx +0 -0
  41. /package/{registry → src/registry/ui}/date-picker.tsx +0 -0
  42. /package/{registry → src/registry/ui}/dialog.tsx +0 -0
  43. /package/{registry → src/registry/ui}/drawer.tsx +0 -0
  44. /package/{registry → src/registry/ui}/dropdown-menu.tsx +0 -0
  45. /package/{registry → src/registry/ui}/file-upload.tsx +0 -0
  46. /package/{registry → src/registry/ui}/hover-card.tsx +0 -0
  47. /package/{registry → src/registry/ui}/image.tsx +0 -0
  48. /package/{registry → src/registry/ui}/input.tsx +0 -0
  49. /package/{registry → src/registry/ui}/kbd.tsx +0 -0
  50. /package/{registry → src/registry/ui}/label.tsx +0 -0
  51. /package/{registry → src/registry/ui}/loading-spinner.tsx +0 -0
  52. /package/{registry → src/registry/ui}/menubar.tsx +0 -0
  53. /package/{registry → src/registry/ui}/native-select.tsx +0 -0
  54. /package/{registry → src/registry/ui}/otp-input.tsx +0 -0
  55. /package/{registry → src/registry/ui}/pagination.tsx +0 -0
  56. /package/{registry → src/registry/ui}/popover.tsx +0 -0
  57. /package/{registry → src/registry/ui}/progress.tsx +0 -0
  58. /package/{registry → src/registry/ui}/radio.tsx +0 -0
  59. /package/{registry → src/registry/ui}/resizable.tsx +0 -0
  60. /package/{registry → src/registry/ui}/scroll-area.tsx +0 -0
  61. /package/{registry → src/registry/ui}/search.tsx +0 -0
  62. /package/{registry → src/registry/ui}/select.tsx +0 -0
  63. /package/{registry → src/registry/ui}/separator.tsx +0 -0
  64. /package/{registry → src/registry/ui}/sheet.tsx +0 -0
  65. /package/{registry → src/registry/ui}/sidebar.tsx +0 -0
  66. /package/{registry → src/registry/ui}/skeleton.tsx +0 -0
  67. /package/{registry → src/registry/ui}/slider.tsx +0 -0
  68. /package/{registry → src/registry/ui}/star-rating.tsx +0 -0
  69. /package/{registry → src/registry/ui}/switch.tsx +0 -0
  70. /package/{registry → src/registry/ui}/table.tsx +0 -0
  71. /package/{registry → src/registry/ui}/tabs.tsx +0 -0
  72. /package/{registry → src/registry/ui}/text.tsx +0 -0
  73. /package/{registry → src/registry/ui}/textarea.tsx +0 -0
  74. /package/{registry → src/registry/ui}/toast.tsx +0 -0
  75. /package/{registry → src/registry/ui}/toggle-group.tsx +0 -0
  76. /package/{registry → src/registry/ui}/toggle.tsx +0 -0
  77. /package/{registry → src/registry/ui}/tooltip.tsx +0 -0
@@ -0,0 +1,183 @@
1
+ /**
2
+ * @srcroot/ui - Stone Theme (Tailwind 4)
3
+ * Warm gray with subtle brown undertones
4
+ */
5
+
6
+ @import "tailwindcss";
7
+
8
+ :root {
9
+ --background: hsl(0 0% 100%);
10
+ --foreground: hsl(24 9.8% 10%);
11
+
12
+ --card: hsl(0 0% 100%);
13
+ --card-foreground: hsl(24 9.8% 10%);
14
+
15
+ --popover: hsl(0 0% 100%);
16
+ --popover-foreground: hsl(24 9.8% 10%);
17
+
18
+ --primary: hsl(24 9.8% 10%);
19
+ --primary-foreground: hsl(60 9.1% 97.8%);
20
+
21
+ --secondary: hsl(60 4.8% 95.9%);
22
+ --secondary-foreground: hsl(24 9.8% 10%);
23
+
24
+ --muted: hsl(60 4.8% 95.9%);
25
+ --muted-foreground: hsl(25 5.3% 44.7%);
26
+
27
+ --accent: hsl(60 4.8% 95.9%);
28
+ --accent-foreground: hsl(24 9.8% 10%);
29
+
30
+ --destructive: hsl(0 84.2% 60.2%);
31
+ --destructive-foreground: hsl(60 9.1% 97.8%);
32
+
33
+ --success: hsl(142.1 76.2% 36.3%);
34
+ --success-foreground: hsl(60 9.1% 97.8%);
35
+
36
+ --warning: hsl(45.4 93.4% 47.5%);
37
+ --warning-foreground: hsl(24 9.8% 10%);
38
+
39
+ --info: hsl(201.3 96.3% 32.2%);
40
+ --info-foreground: hsl(60 9.1% 97.8%);
41
+
42
+ --border: hsl(20 5.9% 90%);
43
+ --input: hsl(20 5.9% 90%);
44
+ --ring: hsl(24 9.8% 10%);
45
+
46
+ --radius: 0.5rem;
47
+
48
+ --sidebar-width: 16rem;
49
+ --sidebar-width-mobile: 18rem;
50
+ --sidebar-width-collapsed: 3rem;
51
+ --sidebar-width-icon: 3rem;
52
+ --header-height: 3.5rem;
53
+
54
+ --sidebar-background: hsl(0 0% 98%);
55
+ --sidebar-foreground: hsl(24 9.8% 10%);
56
+ --sidebar-primary: hsl(24 9.8% 10%);
57
+ --sidebar-primary-foreground: hsl(60 9.1% 97.8%);
58
+ --sidebar-accent: hsl(60 4.8% 95.9%);
59
+ --sidebar-accent-foreground: hsl(24 9.8% 10%);
60
+ --sidebar-border: hsl(20 5.9% 90%);
61
+ --sidebar-ring: hsl(24 9.8% 10%);
62
+ }
63
+
64
+ @theme inline {
65
+ --color-border: var(--border);
66
+ --color-input: var(--input);
67
+ --color-ring: var(--ring);
68
+ --color-background: var(--background);
69
+ --color-foreground: var(--foreground);
70
+
71
+ --color-primary: var(--primary);
72
+ --color-primary-foreground: var(--primary-foreground);
73
+
74
+ --color-secondary: var(--secondary);
75
+ --color-secondary-foreground: var(--secondary-foreground);
76
+
77
+ --color-destructive: var(--destructive);
78
+ --color-destructive-foreground: var(--destructive-foreground);
79
+
80
+ --color-muted: var(--muted);
81
+ --color-muted-foreground: var(--muted-foreground);
82
+
83
+ --color-accent: var(--accent);
84
+ --color-accent-foreground: var(--accent-foreground);
85
+
86
+ --color-popover: var(--popover);
87
+ --color-popover-foreground: var(--popover-foreground);
88
+
89
+ --color-card: var(--card);
90
+ --color-card-foreground: var(--card-foreground);
91
+
92
+ --color-sidebar: var(--sidebar-background);
93
+ --color-sidebar-foreground: var(--sidebar-foreground);
94
+ --color-sidebar-primary: var(--sidebar-primary);
95
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
96
+ --color-sidebar-accent: var(--sidebar-accent);
97
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
98
+ --color-sidebar-border: var(--sidebar-border);
99
+ --color-sidebar-ring: var(--sidebar-ring);
100
+
101
+ --radius-lg: var(--radius);
102
+ --radius-md: calc(var(--radius) - 2px);
103
+ --radius-sm: calc(var(--radius) - 4px);
104
+
105
+ --animate-accordion-down: accordion-down 0.2s ease-out;
106
+ --animate-accordion-up: accordion-up 0.2s ease-out;
107
+
108
+ @keyframes accordion-down {
109
+ from {
110
+ height: 0;
111
+ }
112
+
113
+ to {
114
+ height: var(--radix-accordion-content-height);
115
+ }
116
+ }
117
+
118
+ @keyframes accordion-up {
119
+ from {
120
+ height: var(--radix-accordion-content-height);
121
+ }
122
+
123
+ to {
124
+ height: 0;
125
+ }
126
+ }
127
+ }
128
+
129
+ @media (prefers-color-scheme: dark) {
130
+ :root {
131
+ --background: hsl(24 9.8% 10%);
132
+ --foreground: hsl(60 9.1% 97.8%);
133
+
134
+ --card: hsl(24 9.8% 10%);
135
+ --card-foreground: hsl(60 9.1% 97.8%);
136
+
137
+ --popover: hsl(24 9.8% 10%);
138
+ --popover-foreground: hsl(60 9.1% 97.8%);
139
+
140
+ --primary: hsl(60 9.1% 97.8%);
141
+ --primary-foreground: hsl(24 9.8% 10%);
142
+
143
+ --secondary: hsl(12 6.5% 15.1%);
144
+ --secondary-foreground: hsl(60 9.1% 97.8%);
145
+
146
+ --muted: hsl(12 6.5% 15.1%);
147
+ --muted-foreground: hsl(24 5.4% 63.9%);
148
+
149
+ --accent: hsl(12 6.5% 15.1%);
150
+ --accent-foreground: hsl(60 9.1% 97.8%);
151
+
152
+ --destructive: hsl(0 62.8% 30.6%);
153
+ --destructive-foreground: hsl(60 9.1% 97.8%);
154
+
155
+ --success: hsl(142.1 70.6% 45.3%);
156
+ --success-foreground: hsl(24 9.8% 10%);
157
+
158
+ --warning: hsl(48 96.5% 53.1%);
159
+ --warning-foreground: hsl(24 9.8% 10%);
160
+
161
+ --info: hsl(199.4 95.5% 53.8%);
162
+ --info-foreground: hsl(24 9.8% 10%);
163
+
164
+ --border: hsl(12 6.5% 15.1%);
165
+ --input: hsl(12 6.5% 15.1%);
166
+ --ring: hsl(24 5.7% 82.9%);
167
+
168
+ --sidebar-background: hsl(24 9.8% 14%);
169
+ --sidebar-foreground: hsl(60 9.1% 97.8%);
170
+ --sidebar-primary: hsl(60 9.1% 97.8%);
171
+ --sidebar-primary-foreground: hsl(24 9.8% 10%);
172
+ --sidebar-accent: hsl(12 6.5% 15.1%);
173
+ --sidebar-accent-foreground: hsl(60 9.1% 97.8%);
174
+ --sidebar-border: hsl(12 6.5% 15.1%);
175
+ --sidebar-ring: hsl(24 5.7% 82.9%);
176
+ }
177
+ }
178
+
179
+ body {
180
+ background: var(--background);
181
+ color: var(--foreground);
182
+ font-family: Arial, Helvetica, sans-serif;
183
+ }
@@ -0,0 +1,183 @@
1
+ /**
2
+ * @srcroot/ui - Zinc Theme (Tailwind 4)
3
+ * Cool gray with subtle blue undertones
4
+ */
5
+
6
+ @import "tailwindcss";
7
+
8
+ :root {
9
+ --background: hsl(0 0% 100%);
10
+ --foreground: hsl(240 10% 3.9%);
11
+
12
+ --card: hsl(0 0% 100%);
13
+ --card-foreground: hsl(240 10% 3.9%);
14
+
15
+ --popover: hsl(0 0% 100%);
16
+ --popover-foreground: hsl(240 10% 3.9%);
17
+
18
+ --primary: hsl(240 5.9% 10%);
19
+ --primary-foreground: hsl(0 0% 98%);
20
+
21
+ --secondary: hsl(240 4.8% 95.9%);
22
+ --secondary-foreground: hsl(240 5.9% 10%);
23
+
24
+ --muted: hsl(240 4.8% 95.9%);
25
+ --muted-foreground: hsl(240 3.8% 46.1%);
26
+
27
+ --accent: hsl(240 4.8% 95.9%);
28
+ --accent-foreground: hsl(240 5.9% 10%);
29
+
30
+ --destructive: hsl(0 84.2% 60.2%);
31
+ --destructive-foreground: hsl(0 0% 98%);
32
+
33
+ --success: hsl(142.1 76.2% 36.3%);
34
+ --success-foreground: hsl(0 0% 98%);
35
+
36
+ --warning: hsl(45.4 93.4% 47.5%);
37
+ --warning-foreground: hsl(240 5.9% 10%);
38
+
39
+ --info: hsl(201.3 96.3% 32.2%);
40
+ --info-foreground: hsl(0 0% 98%);
41
+
42
+ --border: hsl(240 5.9% 90%);
43
+ --input: hsl(240 5.9% 90%);
44
+ --ring: hsl(240 10% 3.9%);
45
+
46
+ --radius: 0.5rem;
47
+
48
+ --sidebar-width: 16rem;
49
+ --sidebar-width-mobile: 18rem;
50
+ --sidebar-width-collapsed: 3rem;
51
+ --sidebar-width-icon: 3rem;
52
+ --header-height: 3.5rem;
53
+
54
+ --sidebar-background: hsl(0 0% 98%);
55
+ --sidebar-foreground: hsl(240 5.3% 26.1%);
56
+ --sidebar-primary: hsl(240 5.9% 10%);
57
+ --sidebar-primary-foreground: hsl(0 0% 98%);
58
+ --sidebar-accent: hsl(240 4.8% 95.9%);
59
+ --sidebar-accent-foreground: hsl(240 5.9% 10%);
60
+ --sidebar-border: hsl(220 13% 91%);
61
+ --sidebar-ring: hsl(217.2 91.2% 59.8%);
62
+ }
63
+
64
+ @theme inline {
65
+ --color-border: var(--border);
66
+ --color-input: var(--input);
67
+ --color-ring: var(--ring);
68
+ --color-background: var(--background);
69
+ --color-foreground: var(--foreground);
70
+
71
+ --color-primary: var(--primary);
72
+ --color-primary-foreground: var(--primary-foreground);
73
+
74
+ --color-secondary: var(--secondary);
75
+ --color-secondary-foreground: var(--secondary-foreground);
76
+
77
+ --color-destructive: var(--destructive);
78
+ --color-destructive-foreground: var(--destructive-foreground);
79
+
80
+ --color-muted: var(--muted);
81
+ --color-muted-foreground: var(--muted-foreground);
82
+
83
+ --color-accent: var(--accent);
84
+ --color-accent-foreground: var(--accent-foreground);
85
+
86
+ --color-popover: var(--popover);
87
+ --color-popover-foreground: var(--popover-foreground);
88
+
89
+ --color-card: var(--card);
90
+ --color-card-foreground: var(--card-foreground);
91
+
92
+ --color-sidebar: var(--sidebar-background);
93
+ --color-sidebar-foreground: var(--sidebar-foreground);
94
+ --color-sidebar-primary: var(--sidebar-primary);
95
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
96
+ --color-sidebar-accent: var(--sidebar-accent);
97
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
98
+ --color-sidebar-border: var(--sidebar-border);
99
+ --color-sidebar-ring: var(--sidebar-ring);
100
+
101
+ --radius-lg: var(--radius);
102
+ --radius-md: calc(var(--radius) - 2px);
103
+ --radius-sm: calc(var(--radius) - 4px);
104
+
105
+ --animate-accordion-down: accordion-down 0.2s ease-out;
106
+ --animate-accordion-up: accordion-up 0.2s ease-out;
107
+
108
+ @keyframes accordion-down {
109
+ from {
110
+ height: 0;
111
+ }
112
+
113
+ to {
114
+ height: var(--radix-accordion-content-height);
115
+ }
116
+ }
117
+
118
+ @keyframes accordion-up {
119
+ from {
120
+ height: var(--radix-accordion-content-height);
121
+ }
122
+
123
+ to {
124
+ height: 0;
125
+ }
126
+ }
127
+ }
128
+
129
+ @media (prefers-color-scheme: dark) {
130
+ :root {
131
+ --background: hsl(240 10% 3.9%);
132
+ --foreground: hsl(0 0% 98%);
133
+
134
+ --card: hsl(240 10% 3.9%);
135
+ --card-foreground: hsl(0 0% 98%);
136
+
137
+ --popover: hsl(240 10% 3.9%);
138
+ --popover-foreground: hsl(0 0% 98%);
139
+
140
+ --primary: hsl(0 0% 98%);
141
+ --primary-foreground: hsl(240 5.9% 10%);
142
+
143
+ --secondary: hsl(240 3.7% 15.9%);
144
+ --secondary-foreground: hsl(0 0% 98%);
145
+
146
+ --muted: hsl(240 3.7% 15.9%);
147
+ --muted-foreground: hsl(240 5% 64.9%);
148
+
149
+ --accent: hsl(240 3.7% 15.9%);
150
+ --accent-foreground: hsl(0 0% 98%);
151
+
152
+ --destructive: hsl(0 62.8% 30.6%);
153
+ --destructive-foreground: hsl(0 0% 98%);
154
+
155
+ --success: hsl(142.1 70.6% 45.3%);
156
+ --success-foreground: hsl(240 5.9% 10%);
157
+
158
+ --warning: hsl(48 96.5% 53.1%);
159
+ --warning-foreground: hsl(240 5.9% 10%);
160
+
161
+ --info: hsl(199.4 95.5% 53.8%);
162
+ --info-foreground: hsl(240 5.9% 10%);
163
+
164
+ --border: hsl(240 3.7% 15.9%);
165
+ --input: hsl(240 3.7% 15.9%);
166
+ --ring: hsl(240 4.9% 83.9%);
167
+
168
+ --sidebar-background: hsl(240 5.9% 10%);
169
+ --sidebar-foreground: hsl(240 4.8% 95.9%);
170
+ --sidebar-primary: hsl(224.3 76.3% 48%);
171
+ --sidebar-primary-foreground: hsl(0 0% 100%);
172
+ --sidebar-accent: hsl(240 3.7% 15.9%);
173
+ --sidebar-accent-foreground: hsl(240 4.8% 95.9%);
174
+ --sidebar-border: hsl(240 3.7% 15.9%);
175
+ --sidebar-ring: hsl(217.2 91.2% 59.8%);
176
+ }
177
+ }
178
+
179
+ body {
180
+ background: var(--background);
181
+ color: var(--foreground);
182
+ font-family: Arial, Helvetica, sans-serif;
183
+ }
@@ -4,9 +4,11 @@ import { cn } from "@/lib/utils"
4
4
 
5
5
  interface CarouselContextValue {
6
6
  currentIndex: number
7
- setCurrentIndex: (index: number) => void
7
+ setCurrentIndex: React.Dispatch<React.SetStateAction<number>>
8
8
  itemsCount: number
9
- setItemsCount: (count: number) => void
9
+ setItemsCount: React.Dispatch<React.SetStateAction<number>>
10
+ isTransitioning: boolean
11
+ setIsTransitioning: React.Dispatch<React.SetStateAction<boolean>>
10
12
  }
11
13
 
12
14
  const CarouselContext = React.createContext<CarouselContextValue | null>(null)
@@ -14,44 +16,42 @@ const CarouselContext = React.createContext<CarouselContextValue | null>(null)
14
16
  interface CarouselProps extends React.HTMLAttributes<HTMLDivElement> {
15
17
  /** Auto-play interval in ms (0 to disable) */
16
18
  autoPlay?: number
17
- /** Loop back to start */
19
+ /** Loop back to start (ignored in this infinite implementation as it's always true-ish, but kept for API) */
18
20
  loop?: boolean
19
21
  }
20
22
 
21
23
  /**
22
- * Carousel/Slider component
23
- *
24
- * @example
25
- * <Carousel>
26
- * <CarouselContent>
27
- * <CarouselItem>Slide 1</CarouselItem>
28
- * <CarouselItem>Slide 2</CarouselItem>
29
- * </CarouselContent>
30
- * <CarouselPrevious />
31
- * <CarouselNext />
32
- * </Carousel>
24
+ * Carousel/Slider component with Infinite Looping
33
25
  */
34
26
  const Carousel = React.forwardRef<HTMLDivElement, CarouselProps>(
35
27
  ({ className, children, autoPlay = 0, loop = true, ...props }, ref) => {
36
- const [currentIndex, setCurrentIndex] = React.useState(0)
28
+ // Start at 1 because 0 is the clone of the last item
29
+ const [currentIndex, setCurrentIndex] = React.useState(1)
37
30
  const [itemsCount, setItemsCount] = React.useState(0)
31
+ const [isTransitioning, setIsTransitioning] = React.useState(true)
32
+ const intervalRef = React.useRef<NodeJS.Timeout | null>(null)
38
33
 
39
34
  React.useEffect(() => {
40
- if (autoPlay > 0 && itemsCount > 0) {
41
- const interval = setInterval(() => {
42
- setCurrentIndex((prev) => {
43
- if (prev >= itemsCount - 1) {
44
- return loop ? 0 : prev
45
- }
46
- return prev + 1
47
- })
35
+ if (autoPlay > 0 && itemsCount > 1) {
36
+ intervalRef.current = setInterval(() => {
37
+ setIsTransitioning(true)
38
+ setCurrentIndex((prev) => prev + 1)
48
39
  }, autoPlay)
49
- return () => clearInterval(interval)
40
+ return () => {
41
+ if (intervalRef.current) clearInterval(intervalRef.current)
42
+ }
50
43
  }
51
- }, [autoPlay, itemsCount, loop])
44
+ }, [autoPlay, itemsCount])
52
45
 
53
46
  return (
54
- <CarouselContext.Provider value={{ currentIndex, setCurrentIndex, itemsCount, setItemsCount }}>
47
+ <CarouselContext.Provider value={{
48
+ currentIndex,
49
+ setCurrentIndex,
50
+ itemsCount,
51
+ setItemsCount,
52
+ isTransitioning,
53
+ setIsTransitioning
54
+ }}>
55
55
  <div
56
56
  ref={ref}
57
57
  className={cn("relative", className)}
@@ -72,19 +72,59 @@ const CarouselContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HT
72
72
  const context = React.useContext(CarouselContext)
73
73
  if (!context) throw new Error("CarouselContent must be used within Carousel")
74
74
 
75
- const childrenArray = React.Children.toArray(children)
75
+ const items = React.Children.toArray(children)
76
76
 
77
77
  React.useEffect(() => {
78
- context.setItemsCount(childrenArray.length)
79
- }, [childrenArray.length, context])
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])
80
116
 
81
117
  return (
82
118
  <div ref={ref} className={cn("overflow-hidden", className)} {...props}>
83
119
  <div
84
- className="w-full h-full flex transition-transform duration-300 ease-in-out"
85
- style={{ transform: `translateX(-${context.currentIndex * 100}%)` }}
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}
86
126
  >
87
- {children}
127
+ {displayItems}
88
128
  </div>
89
129
  </div>
90
130
  )
@@ -112,7 +152,10 @@ const CarouselPrevious = React.forwardRef<
112
152
  const context = React.useContext(CarouselContext)
113
153
  if (!context) throw new Error("CarouselPrevious must be used within Carousel")
114
154
 
115
- const canGoPrev = context.currentIndex > 0
155
+ const handlePrev = () => {
156
+ context.setIsTransitioning(true)
157
+ context.setCurrentIndex((prev) => prev - 1)
158
+ }
116
159
 
117
160
  return (
118
161
  <button
@@ -123,8 +166,7 @@ const CarouselPrevious = React.forwardRef<
123
166
  "hover:bg-accent disabled:opacity-50",
124
167
  className
125
168
  )}
126
- disabled={!canGoPrev}
127
- onClick={() => context.setCurrentIndex(context.currentIndex - 1)}
169
+ onClick={handlePrev}
128
170
  aria-label="Previous slide"
129
171
  {...props}
130
172
  >
@@ -143,7 +185,10 @@ const CarouselNext = React.forwardRef<
143
185
  const context = React.useContext(CarouselContext)
144
186
  if (!context) throw new Error("CarouselNext must be used within Carousel")
145
187
 
146
- const canGoNext = context.currentIndex < context.itemsCount - 1
188
+ const handleNext = () => {
189
+ context.setIsTransitioning(true)
190
+ context.setCurrentIndex((prev) => prev + 1)
191
+ }
147
192
 
148
193
  return (
149
194
  <button
@@ -154,8 +199,7 @@ const CarouselNext = React.forwardRef<
154
199
  "hover:bg-accent disabled:opacity-50",
155
200
  className
156
201
  )}
157
- disabled={!canGoNext}
158
- onClick={() => context.setCurrentIndex(context.currentIndex + 1)}
202
+ onClick={handleNext}
159
203
  aria-label="Next slide"
160
204
  {...props}
161
205
  >
@@ -0,0 +1,96 @@
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+ import { cn } from "@/lib/utils"
4
+
5
+ import { ScrollArea } from "@/components/ui/scroll-area"
6
+
7
+ const Chatbot = React.forwardRef<
8
+ HTMLDivElement,
9
+ React.HTMLAttributes<HTMLDivElement>
10
+ >(({ className, ...props }, ref) => (
11
+ <div
12
+ ref={ref}
13
+ className={cn(
14
+ "flex flex-col w-full h-[500px] border rounded-lg bg-background text-foreground shadow-sm overflow-hidden",
15
+ className
16
+ )}
17
+ {...props}
18
+ />
19
+ ))
20
+ Chatbot.displayName = "Chatbot"
21
+
22
+ const ChatbotHeader = React.forwardRef<
23
+ HTMLDivElement,
24
+ React.HTMLAttributes<HTMLDivElement>
25
+ >(({ className, ...props }, ref) => (
26
+ <div
27
+ ref={ref}
28
+ className={cn(
29
+ "flex items-center px-4 py-3 border-b bg-muted/40",
30
+ className
31
+ )}
32
+ {...props}
33
+ />
34
+ ))
35
+ ChatbotHeader.displayName = "ChatbotHeader"
36
+
37
+ const ChatbotContent = React.forwardRef<
38
+ HTMLDivElement,
39
+ React.HTMLAttributes<HTMLDivElement>
40
+ >(({ className, ...props }, ref) => (
41
+ <ScrollArea
42
+ ref={ref}
43
+ className={cn("flex-1 p-2 space-y-4 flex flex-col", className)}
44
+ scrollbarSize="thin"
45
+ {...props}
46
+ />
47
+ ))
48
+ ChatbotContent.displayName = "ChatbotContent"
49
+
50
+ const chatbotMessageVariants = cva(
51
+ "max-w-[80%] rounded-2xl px-4 py-2 text-sm break-words",
52
+ {
53
+ variants: {
54
+ variant: {
55
+ user: "bg-primary text-primary-foreground rounded-br-none ml-auto",
56
+ bot: "bg-muted text-foreground rounded-bl-none mr-auto",
57
+ },
58
+ },
59
+ defaultVariants: {
60
+ variant: "bot",
61
+ },
62
+ }
63
+ )
64
+
65
+ const ChatbotMessage = React.forwardRef<
66
+ HTMLDivElement,
67
+ React.HTMLAttributes<HTMLDivElement> &
68
+ VariantProps<typeof chatbotMessageVariants>
69
+ >(({ className, variant, ...props }, ref) => (
70
+ <div
71
+ ref={ref}
72
+ className={cn(chatbotMessageVariants({ variant }), className)}
73
+ {...props}
74
+ />
75
+ ))
76
+ ChatbotMessage.displayName = "ChatbotMessage"
77
+
78
+ const ChatbotFooter = React.forwardRef<
79
+ HTMLDivElement,
80
+ React.HTMLAttributes<HTMLDivElement>
81
+ >(({ className, ...props }, ref) => (
82
+ <div
83
+ ref={ref}
84
+ className={cn("flex items-center p-3 border-t bg-background rounded-b-lg", className)}
85
+ {...props}
86
+ />
87
+ ))
88
+ ChatbotFooter.displayName = "ChatbotFooter"
89
+
90
+ export {
91
+ Chatbot,
92
+ ChatbotHeader,
93
+ ChatbotContent,
94
+ ChatbotMessage,
95
+ ChatbotFooter,
96
+ }