@tuturuuu/ui 0.0.4

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 (104) hide show
  1. package/.checksum +1 -0
  2. package/README.md +46 -0
  3. package/components.json +20 -0
  4. package/eslint.config.mjs +20 -0
  5. package/jsr.json +10 -0
  6. package/package.json +120 -0
  7. package/postcss.config.mjs +8 -0
  8. package/rollup.config.js +40 -0
  9. package/src/components/ui/accordion.tsx +70 -0
  10. package/src/components/ui/alert-dialog.tsx +156 -0
  11. package/src/components/ui/alert.tsx +58 -0
  12. package/src/components/ui/aspect-ratio.tsx +11 -0
  13. package/src/components/ui/avatar.tsx +52 -0
  14. package/src/components/ui/badge.tsx +49 -0
  15. package/src/components/ui/breadcrumb.tsx +108 -0
  16. package/src/components/ui/button.tsx +61 -0
  17. package/src/components/ui/calendar.tsx +212 -0
  18. package/src/components/ui/card.tsx +74 -0
  19. package/src/components/ui/carousel.tsx +240 -0
  20. package/src/components/ui/chart.tsx +365 -0
  21. package/src/components/ui/checkbox.tsx +31 -0
  22. package/src/components/ui/codeblock.tsx +161 -0
  23. package/src/components/ui/collapsible.tsx +33 -0
  24. package/src/components/ui/color-picker.tsx +143 -0
  25. package/src/components/ui/command.tsx +176 -0
  26. package/src/components/ui/context-menu.tsx +251 -0
  27. package/src/components/ui/custom/autosize-textarea.tsx +111 -0
  28. package/src/components/ui/custom/calendar/core.tsx +61 -0
  29. package/src/components/ui/custom/calendar/day-cell.tsx +74 -0
  30. package/src/components/ui/custom/calendar/month-header.tsx +59 -0
  31. package/src/components/ui/custom/calendar/month-view.tsx +110 -0
  32. package/src/components/ui/custom/calendar/utils.ts +76 -0
  33. package/src/components/ui/custom/calendar/year-calendar.tsx +64 -0
  34. package/src/components/ui/custom/calendar/year-view.tsx +58 -0
  35. package/src/components/ui/custom/combobox.tsx +197 -0
  36. package/src/components/ui/custom/common-footer.tsx +215 -0
  37. package/src/components/ui/custom/compared-date-range-picker.tsx +561 -0
  38. package/src/components/ui/custom/date-input.tsx +279 -0
  39. package/src/components/ui/custom/empty-card.tsx +39 -0
  40. package/src/components/ui/custom/feature-summary.tsx +135 -0
  41. package/src/components/ui/custom/file-uploader.tsx +349 -0
  42. package/src/components/ui/custom/input-field.tsx +29 -0
  43. package/src/components/ui/custom/loading-indicator.tsx +28 -0
  44. package/src/components/ui/custom/modifiable-dialog-trigger.tsx +83 -0
  45. package/src/components/ui/custom/month-picker.tsx +157 -0
  46. package/src/components/ui/custom/report-preview.tsx +175 -0
  47. package/src/components/ui/custom/search-bar.tsx +56 -0
  48. package/src/components/ui/custom/select-field.tsx +78 -0
  49. package/src/components/ui/custom/tables/data-table-column-header.tsx +72 -0
  50. package/src/components/ui/custom/tables/data-table-create-button.tsx +31 -0
  51. package/src/components/ui/custom/tables/data-table-faceted-filter.tsx +142 -0
  52. package/src/components/ui/custom/tables/data-table-pagination.tsx +243 -0
  53. package/src/components/ui/custom/tables/data-table-refresh-button.tsx +45 -0
  54. package/src/components/ui/custom/tables/data-table-toolbar.tsx +133 -0
  55. package/src/components/ui/custom/tables/data-table-view-options.tsx +112 -0
  56. package/src/components/ui/custom/tables/data-table.tsx +228 -0
  57. package/src/components/ui/custom/uploaded-files-card.tsx +50 -0
  58. package/src/components/ui/dialog.tsx +137 -0
  59. package/src/components/ui/drawer.tsx +131 -0
  60. package/src/components/ui/dropdown-menu.tsx +256 -0
  61. package/src/components/ui/form.tsx +167 -0
  62. package/src/components/ui/hover-card.tsx +41 -0
  63. package/src/components/ui/icons.tsx +506 -0
  64. package/src/components/ui/input-otp.tsx +78 -0
  65. package/src/components/ui/input.tsx +18 -0
  66. package/src/components/ui/label.tsx +23 -0
  67. package/src/components/ui/markdown.tsx +7 -0
  68. package/src/components/ui/menubar.tsx +275 -0
  69. package/src/components/ui/navigation-menu.tsx +169 -0
  70. package/src/components/ui/pagination.tsx +126 -0
  71. package/src/components/ui/popover.tsx +47 -0
  72. package/src/components/ui/progress.tsx +30 -0
  73. package/src/components/ui/radio-group.tsx +44 -0
  74. package/src/components/ui/resizable.tsx +55 -0
  75. package/src/components/ui/scroll-area.tsx +57 -0
  76. package/src/components/ui/select.tsx +180 -0
  77. package/src/components/ui/separator.tsx +27 -0
  78. package/src/components/ui/sheet.tsx +138 -0
  79. package/src/components/ui/sidebar.tsx +734 -0
  80. package/src/components/ui/skeleton.tsx +13 -0
  81. package/src/components/ui/slider.tsx +62 -0
  82. package/src/components/ui/sonner.tsx +29 -0
  83. package/src/components/ui/switch.tsx +30 -0
  84. package/src/components/ui/table.tsx +112 -0
  85. package/src/components/ui/tabs.tsx +68 -0
  86. package/src/components/ui/tag-input.tsx +141 -0
  87. package/src/components/ui/textarea.tsx +17 -0
  88. package/src/components/ui/time-picker-input.tsx +117 -0
  89. package/src/components/ui/time-picker-utils.tsx +146 -0
  90. package/src/components/ui/toast.tsx +128 -0
  91. package/src/components/ui/toaster.tsx +35 -0
  92. package/src/components/ui/toggle-group.tsx +72 -0
  93. package/src/components/ui/toggle.tsx +46 -0
  94. package/src/components/ui/tooltip.tsx +60 -0
  95. package/src/globals.css +252 -0
  96. package/src/hooks/use-callback-ref.ts +28 -0
  97. package/src/hooks/use-controllable-state.ts +68 -0
  98. package/src/hooks/use-copy-to-clipboard.ts +46 -0
  99. package/src/hooks/use-form.ts +23 -0
  100. package/src/hooks/use-forwarded-ref.ts +17 -0
  101. package/src/hooks/use-mobile.tsx +21 -0
  102. package/src/hooks/use-toast.ts +191 -0
  103. package/src/resolvers.ts +3 -0
  104. package/tsconfig.json +17 -0
@@ -0,0 +1,108 @@
1
+ import { Slot } from '@radix-ui/react-slot';
2
+ import { cn } from '@tuturuuu/utils/format';
3
+ import { ChevronRight, MoreHorizontal } from 'lucide-react';
4
+ import * as React from 'react';
5
+
6
+ function Breadcrumb({ ...props }: React.ComponentProps<'nav'>) {
7
+ return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />;
8
+ }
9
+
10
+ function BreadcrumbList({ className, ...props }: React.ComponentProps<'ol'>) {
11
+ return (
12
+ <ol
13
+ data-slot="breadcrumb-list"
14
+ className={cn(
15
+ 'flex flex-wrap items-center gap-1.5 text-sm break-words text-muted-foreground sm:gap-2.5',
16
+ className
17
+ )}
18
+ {...props}
19
+ />
20
+ );
21
+ }
22
+
23
+ function BreadcrumbItem({ className, ...props }: React.ComponentProps<'li'>) {
24
+ return (
25
+ <li
26
+ data-slot="breadcrumb-item"
27
+ className={cn('inline-flex items-center gap-1.5', className)}
28
+ {...props}
29
+ />
30
+ );
31
+ }
32
+
33
+ function BreadcrumbLink({
34
+ asChild,
35
+ className,
36
+ ...props
37
+ }: React.ComponentProps<'a'> & {
38
+ asChild?: boolean;
39
+ }) {
40
+ const Comp = asChild ? Slot : 'a';
41
+
42
+ return (
43
+ <Comp
44
+ data-slot="breadcrumb-link"
45
+ className={cn('transition-colors hover:text-foreground', className)}
46
+ {...props}
47
+ />
48
+ );
49
+ }
50
+
51
+ function BreadcrumbPage({ className, ...props }: React.ComponentProps<'span'>) {
52
+ return (
53
+ <span
54
+ data-slot="breadcrumb-page"
55
+ role="link"
56
+ aria-disabled="true"
57
+ aria-current="page"
58
+ className={cn('font-normal text-foreground', className)}
59
+ {...props}
60
+ />
61
+ );
62
+ }
63
+
64
+ function BreadcrumbSeparator({
65
+ children,
66
+ className,
67
+ ...props
68
+ }: React.ComponentProps<'li'>) {
69
+ return (
70
+ <li
71
+ data-slot="breadcrumb-separator"
72
+ role="presentation"
73
+ aria-hidden="true"
74
+ className={cn('[&>svg]:size-3.5', className)}
75
+ {...props}
76
+ >
77
+ {children ?? <ChevronRight />}
78
+ </li>
79
+ );
80
+ }
81
+
82
+ function BreadcrumbEllipsis({
83
+ className,
84
+ ...props
85
+ }: React.ComponentProps<'span'>) {
86
+ return (
87
+ <span
88
+ data-slot="breadcrumb-ellipsis"
89
+ role="presentation"
90
+ aria-hidden="true"
91
+ className={cn('flex size-9 items-center justify-center', className)}
92
+ {...props}
93
+ >
94
+ <MoreHorizontal className="size-4" />
95
+ <span className="sr-only">More</span>
96
+ </span>
97
+ );
98
+ }
99
+
100
+ export {
101
+ Breadcrumb,
102
+ BreadcrumbEllipsis,
103
+ BreadcrumbItem,
104
+ BreadcrumbLink,
105
+ BreadcrumbList,
106
+ BreadcrumbPage,
107
+ BreadcrumbSeparator,
108
+ };
@@ -0,0 +1,61 @@
1
+ import { Slot } from '@radix-ui/react-slot';
2
+ import { cn } from '@tuturuuu/utils/format';
3
+ import { type VariantProps, cva } from 'class-variance-authority';
4
+ import * as React from 'react';
5
+
6
+ const buttonVariants = cva(
7
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 focus-visible:ring-4 focus-visible:outline-1 aria-invalid:focus-visible:ring-0",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default:
12
+ 'bg-primary text-primary-foreground shadow-sm hover:bg-primary/90',
13
+ destructive:
14
+ 'bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90',
15
+ outline:
16
+ 'border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground',
17
+ secondary:
18
+ 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
19
+ ghost: 'hover:bg-accent hover:text-accent-foreground',
20
+ link: 'text-primary underline-offset-4 hover:underline',
21
+ },
22
+ size: {
23
+ default: 'h-9 px-4 py-2 has-[>svg]:px-3',
24
+ xs: 'h-8 rounded-md has-[>svg]:px-2',
25
+ sm: 'h-8 rounded-md px-3 has-[>svg]:px-2.5',
26
+ lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
27
+ icon: 'size-9',
28
+ },
29
+ },
30
+ defaultVariants: {
31
+ variant: 'default',
32
+ size: 'default',
33
+ },
34
+ }
35
+ );
36
+
37
+ function Button({
38
+ className,
39
+ variant,
40
+ size,
41
+ asChild = false,
42
+ ...props
43
+ }: React.ComponentProps<'button'> &
44
+ VariantProps<typeof buttonVariants> & {
45
+ asChild?: boolean;
46
+ }) {
47
+ const Comp = asChild ? Slot : 'button';
48
+
49
+ return (
50
+ <Comp
51
+ data-slot="button"
52
+ className={cn(buttonVariants({ variant, size, className }))}
53
+ {...props}
54
+ />
55
+ );
56
+ }
57
+
58
+ type ButtonProps = React.ComponentProps<typeof Button>;
59
+
60
+ export { Button, buttonVariants };
61
+ export type { ButtonProps };
@@ -0,0 +1,212 @@
1
+ 'use client';
2
+
3
+ import { buttonVariants } from './button';
4
+ import { DateInput } from './custom/date-input';
5
+ import {
6
+ Select,
7
+ SelectContent,
8
+ SelectItem,
9
+ SelectTrigger,
10
+ SelectValue,
11
+ } from './select';
12
+ import { cn } from '@tuturuuu/utils/format';
13
+ import { format } from 'date-fns';
14
+ import { ChevronLeft, ChevronRight } from 'lucide-react';
15
+ import * as React from 'react';
16
+ import { DayPicker } from 'react-day-picker';
17
+
18
+ export type CalendarProps = React.ComponentProps<typeof DayPicker> & {
19
+ // eslint-disable-next-line no-unused-vars
20
+ onSubmit?: (date: Date) => void;
21
+ };
22
+
23
+ function Calendar({
24
+ className,
25
+ classNames,
26
+ showOutsideDays = true,
27
+ onSubmit,
28
+ ...props
29
+ }: CalendarProps) {
30
+ const defaultMonth = props.defaultMonth || new Date();
31
+ const [month, setMonth] = React.useState<Date>(defaultMonth);
32
+
33
+ const years = Array.from({ length: 200 }, (_, i) => {
34
+ const year = new Date().getFullYear() - 100 + i;
35
+ return { value: year.toString(), label: year.toString() };
36
+ });
37
+
38
+ const months = Array.from({ length: 12 }, (_, i) => {
39
+ const month = new Date(2024, i, 1);
40
+ return {
41
+ value: i.toString(),
42
+ label: format(month, 'MMMM'),
43
+ };
44
+ });
45
+
46
+ const currentYear = new Date().getFullYear();
47
+ const isCurrentYear = month.getFullYear() === currentYear;
48
+ const isCurrentMonth =
49
+ month.getMonth() === new Date().getMonth() && isCurrentYear;
50
+
51
+ return (
52
+ <div className="space-y-4">
53
+ {props.mode === 'single' && (
54
+ <div className="flex items-center justify-center border-b p-2">
55
+ <DateInput
56
+ value={props.selected as Date}
57
+ onChange={props.onSelect as any}
58
+ onSubmit={onSubmit}
59
+ />
60
+ </div>
61
+ )}
62
+
63
+ <div>
64
+ <div className="flex items-center justify-between gap-2 border-b px-2 pb-4">
65
+ <button
66
+ onClick={() => {
67
+ const prev = new Date(month);
68
+ prev.setMonth(prev.getMonth() - 1);
69
+ setMonth(prev);
70
+ }}
71
+ className={cn(
72
+ buttonVariants({ variant: 'ghost', size: 'icon' }),
73
+ 'h-7 w-7 transition-colors hover:bg-accent/50'
74
+ )}
75
+ >
76
+ <ChevronLeft className="h-4 w-4" />
77
+ </button>
78
+
79
+ <div className="flex items-center gap-2">
80
+ <Select
81
+ value={month.getFullYear().toString()}
82
+ onValueChange={(year) => {
83
+ const newDate = new Date(month);
84
+ newDate.setFullYear(parseInt(year));
85
+ setMonth(newDate);
86
+ }}
87
+ >
88
+ <SelectTrigger
89
+ className={cn(
90
+ 'h-8 w-[90px] transition-colors',
91
+ isCurrentYear && 'font-medium text-primary'
92
+ )}
93
+ >
94
+ <SelectValue placeholder="Year" />
95
+ </SelectTrigger>
96
+ <SelectContent
97
+ position="popper"
98
+ className="h-[300px] overflow-y-auto"
99
+ >
100
+ <div className="sticky top-0 -mx-1 flex items-center justify-center border-b bg-background py-1">
101
+ <div className="px-2 text-sm font-medium text-muted-foreground">
102
+ {currentYear}
103
+ </div>
104
+ </div>
105
+ {years.map((year) => (
106
+ <SelectItem
107
+ key={year.value}
108
+ value={year.value}
109
+ className={cn(
110
+ 'transition-colors',
111
+ parseInt(year.value) === currentYear &&
112
+ 'font-medium text-primary'
113
+ )}
114
+ >
115
+ {year.label}
116
+ </SelectItem>
117
+ ))}
118
+ </SelectContent>
119
+ </Select>
120
+
121
+ <Select
122
+ value={month.getMonth().toString()}
123
+ onValueChange={(monthValue) => {
124
+ const newDate = new Date(month);
125
+ newDate.setMonth(parseInt(monthValue));
126
+ setMonth(newDate);
127
+ }}
128
+ >
129
+ <SelectTrigger
130
+ className={cn(
131
+ 'h-8 w-[130px] transition-colors',
132
+ isCurrentMonth && 'font-medium text-primary'
133
+ )}
134
+ >
135
+ <SelectValue placeholder="Month" />
136
+ </SelectTrigger>
137
+ <SelectContent position="popper">
138
+ {months.map((month) => (
139
+ <SelectItem
140
+ key={month.value}
141
+ value={month.value}
142
+ className="capitalize"
143
+ >
144
+ {month.label}
145
+ </SelectItem>
146
+ ))}
147
+ </SelectContent>
148
+ </Select>
149
+ </div>
150
+
151
+ <button
152
+ onClick={() => {
153
+ const next = new Date(month);
154
+ next.setMonth(next.getMonth() + 1);
155
+ setMonth(next);
156
+ }}
157
+ className={cn(
158
+ buttonVariants({ variant: 'ghost', size: 'icon' }),
159
+ 'h-7 w-7 transition-colors hover:bg-accent/50'
160
+ )}
161
+ >
162
+ <ChevronRight className="h-4 w-4" />
163
+ </button>
164
+ </div>
165
+
166
+ <DayPicker
167
+ {...props}
168
+ month={month}
169
+ onMonthChange={setMonth}
170
+ defaultMonth={defaultMonth}
171
+ showOutsideDays={showOutsideDays}
172
+ className={cn('', className)}
173
+ classNames={{
174
+ root: 'bg-transparent',
175
+ months: 'flex flex-col',
176
+ month:
177
+ 'space-y-4 min-w-[276px] text-center p-2 font-semibold shrink-0',
178
+ caption: 'hidden',
179
+ nav: 'hidden',
180
+ nav_button: 'hidden',
181
+ table: 'w-full border-collapse',
182
+ head_row: 'grid grid-cols-7 gap-1',
183
+ weekday:
184
+ 'text-muted-foreground rounded-md font-normal text-[0.8rem] text-center',
185
+ row: 'grid grid-cols-7 gap-1 mt-2',
186
+ day: 'text-center text-sm p-0 relative w-9',
187
+ day_button: cn(
188
+ buttonVariants({ variant: 'ghost' }),
189
+ 'h-9 w-full rounded-md p-0 font-normal transition-colors duration-300',
190
+ 'aria-selected:bg-foreground aria-selected:text-background',
191
+ 'hover:bg-accent/50 hover:text-accent-foreground',
192
+ 'hover:aria-selected:bg-foreground hover:aria-selected:text-background'
193
+ ),
194
+ selected: '!bg-foreground !text-background rounded-md',
195
+ today: 'bg-accent text-accent-foreground rounded-md font-medium',
196
+ outside: 'text-muted-foreground opacity-50',
197
+ disabled: 'text-muted-foreground opacity-50',
198
+ range_start: '!bg-foreground !text-background rounded-l-md',
199
+ range_end: '!bg-foreground !text-background rounded-r-md',
200
+ range_middle: 'aria-selected:bg-foreground/20',
201
+ hidden: 'invisible',
202
+ month_grid: 'w-full',
203
+ ...classNames,
204
+ }}
205
+ fixedWeeks
206
+ />
207
+ </div>
208
+ </div>
209
+ );
210
+ }
211
+
212
+ export { Calendar };
@@ -0,0 +1,74 @@
1
+ import { cn } from '@tuturuuu/utils/format';
2
+ import * as React from 'react';
3
+
4
+ function Card({ className, ...props }: React.ComponentProps<'div'>) {
5
+ return (
6
+ <div
7
+ data-slot="card"
8
+ className={cn(
9
+ 'rounded-xl border bg-card text-card-foreground shadow-sm',
10
+ className
11
+ )}
12
+ {...props}
13
+ />
14
+ );
15
+ }
16
+
17
+ function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
18
+ return (
19
+ <div
20
+ data-slot="card-header"
21
+ className={cn('flex flex-col gap-1.5 p-6', className)}
22
+ {...props}
23
+ />
24
+ );
25
+ }
26
+
27
+ function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
28
+ return (
29
+ <div
30
+ data-slot="card-title"
31
+ className={cn('leading-none font-semibold tracking-tight', className)}
32
+ {...props}
33
+ />
34
+ );
35
+ }
36
+
37
+ function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
38
+ return (
39
+ <div
40
+ data-slot="card-description"
41
+ className={cn('text-sm text-muted-foreground', className)}
42
+ {...props}
43
+ />
44
+ );
45
+ }
46
+
47
+ function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
48
+ return (
49
+ <div
50
+ data-slot="card-content"
51
+ className={cn('p-6 pt-0', className)}
52
+ {...props}
53
+ />
54
+ );
55
+ }
56
+
57
+ function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
58
+ return (
59
+ <div
60
+ data-slot="card-footer"
61
+ className={cn('flex items-center p-6 pt-0', className)}
62
+ {...props}
63
+ />
64
+ );
65
+ }
66
+
67
+ export {
68
+ Card,
69
+ CardContent,
70
+ CardDescription,
71
+ CardFooter,
72
+ CardHeader,
73
+ CardTitle,
74
+ };
@@ -0,0 +1,240 @@
1
+ 'use client';
2
+
3
+ import { Button } from './button';
4
+ import { cn } from '@tuturuuu/utils/format';
5
+ import useEmblaCarousel, {
6
+ type UseEmblaCarouselType,
7
+ } from 'embla-carousel-react';
8
+ import { ArrowLeft, ArrowRight } from 'lucide-react';
9
+ import * as React from 'react';
10
+
11
+ type CarouselApi = UseEmblaCarouselType[1];
12
+ type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
13
+ type CarouselOptions = UseCarouselParameters[0];
14
+ type CarouselPlugin = UseCarouselParameters[1];
15
+
16
+ type CarouselProps = {
17
+ opts?: CarouselOptions;
18
+ plugins?: CarouselPlugin;
19
+ orientation?: 'horizontal' | 'vertical';
20
+ setApi?: (api: CarouselApi) => void;
21
+ };
22
+
23
+ type CarouselContextProps = {
24
+ carouselRef: ReturnType<typeof useEmblaCarousel>[0];
25
+ api: ReturnType<typeof useEmblaCarousel>[1];
26
+ scrollPrev: () => void;
27
+ scrollNext: () => void;
28
+ canScrollPrev: boolean;
29
+ canScrollNext: boolean;
30
+ } & CarouselProps;
31
+
32
+ const CarouselContext = React.createContext<CarouselContextProps | null>(null);
33
+
34
+ function useCarousel() {
35
+ const context = React.useContext(CarouselContext);
36
+
37
+ if (!context) {
38
+ throw new Error('useCarousel must be used within a <Carousel />');
39
+ }
40
+
41
+ return context;
42
+ }
43
+
44
+ function Carousel({
45
+ orientation = 'horizontal',
46
+ opts,
47
+ setApi,
48
+ plugins,
49
+ className,
50
+ children,
51
+ ...props
52
+ }: React.ComponentProps<'div'> & CarouselProps) {
53
+ const [carouselRef, api] = useEmblaCarousel(
54
+ {
55
+ ...opts,
56
+ axis: orientation === 'horizontal' ? 'x' : 'y',
57
+ },
58
+ plugins
59
+ );
60
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false);
61
+ const [canScrollNext, setCanScrollNext] = React.useState(false);
62
+
63
+ const onSelect = React.useCallback((api: CarouselApi) => {
64
+ if (!api) return;
65
+ setCanScrollPrev(api.canScrollPrev());
66
+ setCanScrollNext(api.canScrollNext());
67
+ }, []);
68
+
69
+ const scrollPrev = React.useCallback(() => {
70
+ api?.scrollPrev();
71
+ }, [api]);
72
+
73
+ const scrollNext = React.useCallback(() => {
74
+ api?.scrollNext();
75
+ }, [api]);
76
+
77
+ const handleKeyDown = React.useCallback(
78
+ (event: React.KeyboardEvent<HTMLDivElement>) => {
79
+ if (event.key === 'ArrowLeft') {
80
+ event.preventDefault();
81
+ scrollPrev();
82
+ } else if (event.key === 'ArrowRight') {
83
+ event.preventDefault();
84
+ scrollNext();
85
+ }
86
+ },
87
+ [scrollPrev, scrollNext]
88
+ );
89
+
90
+ React.useEffect(() => {
91
+ if (!api || !setApi) return;
92
+ setApi(api);
93
+ }, [api, setApi]);
94
+
95
+ React.useEffect(() => {
96
+ if (!api) return;
97
+ onSelect(api);
98
+ api.on('reInit', onSelect);
99
+ api.on('select', onSelect);
100
+
101
+ return () => {
102
+ api?.off('select', onSelect);
103
+ };
104
+ }, [api, onSelect]);
105
+
106
+ return (
107
+ <CarouselContext.Provider
108
+ value={{
109
+ carouselRef,
110
+ api: api,
111
+ opts,
112
+ orientation:
113
+ orientation || (opts?.axis === 'y' ? 'vertical' : 'horizontal'),
114
+ scrollPrev,
115
+ scrollNext,
116
+ canScrollPrev,
117
+ canScrollNext,
118
+ }}
119
+ >
120
+ <div
121
+ onKeyDownCapture={handleKeyDown}
122
+ className={cn('relative', className)}
123
+ role="region"
124
+ aria-roledescription="carousel"
125
+ data-slot="carousel"
126
+ {...props}
127
+ >
128
+ {children}
129
+ </div>
130
+ </CarouselContext.Provider>
131
+ );
132
+ }
133
+
134
+ function CarouselContent({ className, ...props }: React.ComponentProps<'div'>) {
135
+ const { carouselRef, orientation } = useCarousel();
136
+
137
+ return (
138
+ <div
139
+ ref={carouselRef}
140
+ className="overflow-hidden"
141
+ data-slot="carousel-content"
142
+ >
143
+ <div
144
+ className={cn(
145
+ 'flex',
146
+ orientation === 'horizontal' ? '-ml-4' : '-mt-4 flex-col',
147
+ className
148
+ )}
149
+ {...props}
150
+ />
151
+ </div>
152
+ );
153
+ }
154
+
155
+ function CarouselItem({ className, ...props }: React.ComponentProps<'div'>) {
156
+ const { orientation } = useCarousel();
157
+
158
+ return (
159
+ <div
160
+ role="group"
161
+ aria-roledescription="slide"
162
+ data-slot="carousel-item"
163
+ className={cn(
164
+ 'min-w-0 shrink-0 grow-0 basis-full',
165
+ orientation === 'horizontal' ? 'pl-4' : 'pt-4',
166
+ className
167
+ )}
168
+ {...props}
169
+ />
170
+ );
171
+ }
172
+
173
+ function CarouselPrevious({
174
+ className,
175
+ variant = 'outline',
176
+ size = 'icon',
177
+ ...props
178
+ }: React.ComponentProps<typeof Button>) {
179
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel();
180
+
181
+ return (
182
+ <Button
183
+ data-slot="carousel-previous"
184
+ variant={variant}
185
+ size={size}
186
+ className={cn(
187
+ 'absolute size-8 rounded-full',
188
+ orientation === 'horizontal'
189
+ ? 'top-1/2 -left-12 -translate-y-1/2'
190
+ : '-top-12 left-1/2 -translate-x-1/2 rotate-90',
191
+ className
192
+ )}
193
+ disabled={!canScrollPrev}
194
+ onClick={scrollPrev}
195
+ {...props}
196
+ >
197
+ <ArrowLeft />
198
+ <span className="sr-only">Previous slide</span>
199
+ </Button>
200
+ );
201
+ }
202
+
203
+ function CarouselNext({
204
+ className,
205
+ variant = 'outline',
206
+ size = 'icon',
207
+ ...props
208
+ }: React.ComponentProps<typeof Button>) {
209
+ const { orientation, scrollNext, canScrollNext } = useCarousel();
210
+
211
+ return (
212
+ <Button
213
+ data-slot="carousel-next"
214
+ variant={variant}
215
+ size={size}
216
+ className={cn(
217
+ 'absolute size-8 rounded-full',
218
+ orientation === 'horizontal'
219
+ ? 'top-1/2 -right-12 -translate-y-1/2'
220
+ : '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',
221
+ className
222
+ )}
223
+ disabled={!canScrollNext}
224
+ onClick={scrollNext}
225
+ {...props}
226
+ >
227
+ <ArrowRight />
228
+ <span className="sr-only">Next slide</span>
229
+ </Button>
230
+ );
231
+ }
232
+
233
+ export {
234
+ Carousel,
235
+ CarouselContent,
236
+ CarouselItem,
237
+ CarouselNext,
238
+ CarouselPrevious,
239
+ type CarouselApi,
240
+ };