@trycompai/design-system 1.0.0

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 (71) hide show
  1. package/README.md +110 -0
  2. package/components.json +21 -0
  3. package/hooks/use-mobile.tsx +19 -0
  4. package/lib/utils.ts +6 -0
  5. package/package.json +103 -0
  6. package/postcss.config.mjs +8 -0
  7. package/src/components/ui/accordion.tsx +60 -0
  8. package/src/components/ui/alert-dialog.tsx +161 -0
  9. package/src/components/ui/alert.tsx +109 -0
  10. package/src/components/ui/aspect-ratio.tsx +21 -0
  11. package/src/components/ui/avatar.tsx +74 -0
  12. package/src/components/ui/badge.tsx +48 -0
  13. package/src/components/ui/breadcrumb.tsx +254 -0
  14. package/src/components/ui/button-group.tsx +89 -0
  15. package/src/components/ui/button.tsx +122 -0
  16. package/src/components/ui/calendar.tsx +190 -0
  17. package/src/components/ui/card.tsx +155 -0
  18. package/src/components/ui/carousel.tsx +216 -0
  19. package/src/components/ui/chart.tsx +325 -0
  20. package/src/components/ui/checkbox.tsx +22 -0
  21. package/src/components/ui/collapsible.tsx +17 -0
  22. package/src/components/ui/combobox.tsx +248 -0
  23. package/src/components/ui/command.tsx +189 -0
  24. package/src/components/ui/container.tsx +34 -0
  25. package/src/components/ui/context-menu.tsx +235 -0
  26. package/src/components/ui/dialog.tsx +122 -0
  27. package/src/components/ui/drawer.tsx +102 -0
  28. package/src/components/ui/dropdown-menu.tsx +242 -0
  29. package/src/components/ui/empty.tsx +94 -0
  30. package/src/components/ui/field.tsx +215 -0
  31. package/src/components/ui/grid.tsx +135 -0
  32. package/src/components/ui/heading.tsx +56 -0
  33. package/src/components/ui/hover-card.tsx +46 -0
  34. package/src/components/ui/index.ts +61 -0
  35. package/src/components/ui/input-group.tsx +128 -0
  36. package/src/components/ui/input-otp.tsx +84 -0
  37. package/src/components/ui/input.tsx +15 -0
  38. package/src/components/ui/item.tsx +188 -0
  39. package/src/components/ui/kbd.tsx +26 -0
  40. package/src/components/ui/label.tsx +15 -0
  41. package/src/components/ui/menubar.tsx +163 -0
  42. package/src/components/ui/navigation-menu.tsx +147 -0
  43. package/src/components/ui/page-header.tsx +51 -0
  44. package/src/components/ui/page-layout.tsx +65 -0
  45. package/src/components/ui/pagination.tsx +104 -0
  46. package/src/components/ui/popover.tsx +57 -0
  47. package/src/components/ui/progress.tsx +61 -0
  48. package/src/components/ui/radio-group.tsx +37 -0
  49. package/src/components/ui/resizable.tsx +41 -0
  50. package/src/components/ui/scroll-area.tsx +48 -0
  51. package/src/components/ui/section.tsx +64 -0
  52. package/src/components/ui/select.tsx +166 -0
  53. package/src/components/ui/separator.tsx +17 -0
  54. package/src/components/ui/sheet.tsx +104 -0
  55. package/src/components/ui/sidebar.tsx +707 -0
  56. package/src/components/ui/skeleton.tsx +5 -0
  57. package/src/components/ui/slider.tsx +51 -0
  58. package/src/components/ui/sonner.tsx +43 -0
  59. package/src/components/ui/spinner.tsx +14 -0
  60. package/src/components/ui/stack.tsx +72 -0
  61. package/src/components/ui/switch.tsx +26 -0
  62. package/src/components/ui/table.tsx +65 -0
  63. package/src/components/ui/tabs.tsx +69 -0
  64. package/src/components/ui/text.tsx +59 -0
  65. package/src/components/ui/textarea.tsx +13 -0
  66. package/src/components/ui/toggle-group.tsx +87 -0
  67. package/src/components/ui/toggle.tsx +42 -0
  68. package/src/components/ui/tooltip.tsx +52 -0
  69. package/src/index.ts +3 -0
  70. package/src/styles/globals.css +122 -0
  71. package/tailwind.config.ts +59 -0
@@ -0,0 +1,190 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import {
5
+ DayPicker,
6
+ getDefaultClassNames,
7
+ type DayButton,
8
+ type DayPickerProps,
9
+ } from 'react-day-picker';
10
+
11
+ import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from 'lucide-react';
12
+ import { cn } from '../../../lib/utils';
13
+ import { Button, buttonVariants } from './button';
14
+
15
+ type CalendarButtonVariant = 'default' | 'outline' | 'secondary' | 'ghost' | 'destructive' | 'link';
16
+
17
+ interface CalendarBaseProps {
18
+ buttonVariant?: CalendarButtonVariant;
19
+ }
20
+
21
+ // Calendar accepts all DayPickerProps except className/classNames (we control those internally)
22
+ type CalendarProps = CalendarBaseProps & DayPickerProps;
23
+
24
+ function Calendar({
25
+ showOutsideDays = true,
26
+ captionLayout = 'label',
27
+ buttonVariant = 'ghost',
28
+ formatters,
29
+ components,
30
+ className: _className,
31
+ classNames: _classNames,
32
+ ...props
33
+ }: CalendarProps) {
34
+ const defaultClassNames = getDefaultClassNames();
35
+
36
+ return (
37
+ <DayPicker
38
+ showOutsideDays={showOutsideDays}
39
+ className={cn(
40
+ 'p-3 [--cell-radius:var(--radius-md)] [--cell-size:--spacing(8)] bg-background group/calendar [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent',
41
+ String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
42
+ String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
43
+ )}
44
+ captionLayout={captionLayout}
45
+ formatters={{
46
+ formatMonthDropdown: (date) => date.toLocaleString('default', { month: 'short' }),
47
+ ...formatters,
48
+ }}
49
+ classNames={{
50
+ root: cn('w-fit', defaultClassNames.root),
51
+ months: cn('flex gap-4 flex-col md:flex-row relative', defaultClassNames.months),
52
+ month: cn('flex flex-col w-full gap-4', defaultClassNames.month),
53
+ nav: cn(
54
+ 'flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between',
55
+ defaultClassNames.nav,
56
+ ),
57
+ button_previous: cn(
58
+ buttonVariants({ variant: buttonVariant }),
59
+ 'size-(--cell-size) aria-disabled:opacity-50 p-0 select-none',
60
+ defaultClassNames.button_previous,
61
+ ),
62
+ button_next: cn(
63
+ buttonVariants({ variant: buttonVariant }),
64
+ 'size-(--cell-size) aria-disabled:opacity-50 p-0 select-none',
65
+ defaultClassNames.button_next,
66
+ ),
67
+ month_caption: cn(
68
+ 'flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)',
69
+ defaultClassNames.month_caption,
70
+ ),
71
+ dropdowns: cn(
72
+ 'w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5',
73
+ defaultClassNames.dropdowns,
74
+ ),
75
+ dropdown_root: cn(
76
+ 'relative cn-calendar-dropdown-root rounded-(--cell-radius)',
77
+ defaultClassNames.dropdown_root,
78
+ ),
79
+ dropdown: cn('absolute bg-popover inset-0 opacity-0', defaultClassNames.dropdown),
80
+ caption_label: cn(
81
+ 'select-none font-medium',
82
+ captionLayout === 'label'
83
+ ? 'text-sm'
84
+ : 'cn-calendar-caption-label rounded-(--cell-radius) flex items-center gap-1 text-sm [&>svg]:text-muted-foreground [&>svg]:size-3.5',
85
+ defaultClassNames.caption_label,
86
+ ),
87
+ table: 'w-full border-collapse',
88
+ weekdays: cn('flex', defaultClassNames.weekdays),
89
+ weekday: cn(
90
+ 'text-muted-foreground rounded-(--cell-radius) flex-1 font-normal text-[0.8rem] select-none',
91
+ defaultClassNames.weekday,
92
+ ),
93
+ week: cn('flex w-full mt-2', defaultClassNames.week),
94
+ week_number_header: cn('select-none w-(--cell-size)', defaultClassNames.week_number_header),
95
+ week_number: cn(
96
+ 'text-[0.8rem] select-none text-muted-foreground',
97
+ defaultClassNames.week_number,
98
+ ),
99
+ day: cn(
100
+ 'relative w-full rounded-(--cell-radius) h-full p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r-(--cell-radius) group/day aspect-square select-none',
101
+ props.showWeekNumber
102
+ ? '[&:nth-child(2)[data-selected=true]_button]:rounded-l-(--cell-radius)'
103
+ : '[&:first-child[data-selected=true]_button]:rounded-l-(--cell-radius)',
104
+ defaultClassNames.day,
105
+ ),
106
+ range_start: cn(
107
+ 'rounded-l-(--cell-radius) bg-muted relative after:bg-muted after:absolute after:inset-y-0 after:w-4 after:right-0 -z-0 isolate',
108
+ defaultClassNames.range_start,
109
+ ),
110
+ range_middle: cn('rounded-none', defaultClassNames.range_middle),
111
+ range_end: cn(
112
+ 'rounded-r-(--cell-radius) bg-muted relative after:bg-muted after:absolute after:inset-y-0 after:w-4 after:left-0 -z-0 isolate',
113
+ defaultClassNames.range_end,
114
+ ),
115
+ today: cn(
116
+ 'bg-muted text-foreground rounded-(--cell-radius) data-[selected=true]:rounded-none',
117
+ defaultClassNames.today,
118
+ ),
119
+ outside: cn(
120
+ 'text-muted-foreground aria-selected:text-muted-foreground',
121
+ defaultClassNames.outside,
122
+ ),
123
+ disabled: cn('text-muted-foreground opacity-50', defaultClassNames.disabled),
124
+ hidden: cn('invisible', defaultClassNames.hidden),
125
+ }}
126
+ components={{
127
+ Root: ({ rootRef, ...componentProps }) => {
128
+ return <div data-slot="calendar" ref={rootRef} {...componentProps} />;
129
+ },
130
+ Chevron: ({ orientation, ...componentProps }) => {
131
+ if (orientation === 'left') {
132
+ return <ChevronLeftIcon className="size-4" {...componentProps} />;
133
+ }
134
+
135
+ if (orientation === 'right') {
136
+ return <ChevronRightIcon className="size-4" {...componentProps} />;
137
+ }
138
+
139
+ return <ChevronDownIcon className="size-4" {...componentProps} />;
140
+ },
141
+ DayButton: CalendarDayButton,
142
+ WeekNumber: ({ children, ...componentProps }) => {
143
+ return (
144
+ <td {...componentProps}>
145
+ <div className="flex size-(--cell-size) items-center justify-center text-center">
146
+ {children}
147
+ </div>
148
+ </td>
149
+ );
150
+ },
151
+ ...components,
152
+ }}
153
+ {...props}
154
+ />
155
+ );
156
+ }
157
+
158
+ function CalendarDayButton({
159
+ day,
160
+ modifiers,
161
+ className: _className,
162
+ ...props
163
+ }: React.ComponentProps<typeof DayButton>) {
164
+ const ref = React.useRef<HTMLButtonElement>(null);
165
+ React.useEffect(() => {
166
+ if (modifiers.focused) ref.current?.focus();
167
+ }, [modifiers.focused]);
168
+
169
+ return (
170
+ <Button
171
+ ref={ref}
172
+ variant="ghost"
173
+ size="calendar-day"
174
+ data-day={day.date.toLocaleDateString()}
175
+ data-selected-single={
176
+ modifiers.selected &&
177
+ !modifiers.range_start &&
178
+ !modifiers.range_end &&
179
+ !modifiers.range_middle
180
+ }
181
+ data-range-start={modifiers.range_start}
182
+ data-range-end={modifiers.range_end}
183
+ data-range-middle={modifiers.range_middle}
184
+ {...props}
185
+ />
186
+ );
187
+ }
188
+
189
+ export { Calendar, CalendarDayButton };
190
+ export type { CalendarProps };
@@ -0,0 +1,155 @@
1
+ import { cva, type VariantProps } from 'class-variance-authority';
2
+ import * as React from 'react';
3
+
4
+ const cardVariants = cva(
5
+ 'ring-foreground/10 bg-card text-card-foreground gap-4 overflow-hidden rounded-xl py-4 text-sm ring-1 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl group/card flex flex-col',
6
+ {
7
+ variants: {
8
+ width: {
9
+ auto: '', // shrink to content (default behavior)
10
+ full: 'w-full', // fill parent container
11
+ sm: 'w-sm', // 24rem (384px)
12
+ md: 'w-md', // 28rem (448px)
13
+ lg: 'w-lg', // 32rem (512px)
14
+ xl: 'w-xl', // 36rem (576px)
15
+ '2xl': 'w-2xl', // 42rem (672px)
16
+ '3xl': 'w-3xl', // 48rem (768px)
17
+ },
18
+ maxWidth: {
19
+ sm: 'max-w-sm',
20
+ md: 'max-w-md',
21
+ lg: 'max-w-lg',
22
+ xl: 'max-w-xl',
23
+ '2xl': 'max-w-2xl',
24
+ '3xl': 'max-w-3xl',
25
+ full: 'max-w-full',
26
+ },
27
+ spacing: {
28
+ default: '',
29
+ tight: 'gap-3',
30
+ relaxed: 'gap-6',
31
+ },
32
+ },
33
+ defaultVariants: {
34
+ width: 'auto',
35
+ spacing: 'default',
36
+ },
37
+ },
38
+ );
39
+
40
+ interface CardProps
41
+ extends
42
+ Omit<React.ComponentProps<'div'>, 'className' | 'title'>,
43
+ VariantProps<typeof cardVariants> {
44
+ size?: 'default' | 'sm';
45
+ /** Card title - renders in CardHeader */
46
+ title?: React.ReactNode;
47
+ /** Card description - renders below title in CardHeader */
48
+ description?: React.ReactNode;
49
+ /** Action element(s) in the header (e.g., buttons, badges) */
50
+ headerAction?: React.ReactNode;
51
+ /** Footer content */
52
+ footer?: React.ReactNode;
53
+ }
54
+
55
+ function Card({
56
+ size = 'default',
57
+ width,
58
+ maxWidth,
59
+ title,
60
+ description,
61
+ headerAction,
62
+ footer,
63
+ children,
64
+ ...props
65
+ }: CardProps) {
66
+ const hasHeader = title || description || headerAction;
67
+
68
+ // Check if children contain compound components (have data-slot)
69
+ const hasCompoundChildren = React.Children.toArray(children).some((child) => {
70
+ if (React.isValidElement(child)) {
71
+ const props = child.props as Record<string, unknown>;
72
+ return (
73
+ props['data-slot'] === 'card-header' ||
74
+ props['data-slot'] === 'card-content' ||
75
+ props['data-slot'] === 'card-footer'
76
+ );
77
+ }
78
+ return false;
79
+ });
80
+
81
+ return (
82
+ <div data-slot="card" data-size={size} className={cardVariants({ width, maxWidth })} {...props}>
83
+ {hasHeader && (
84
+ <CardHeader>
85
+ {title && <CardTitle>{title}</CardTitle>}
86
+ {description && <CardDescription>{description}</CardDescription>}
87
+ {headerAction && <CardAction>{headerAction}</CardAction>}
88
+ </CardHeader>
89
+ )}
90
+ {hasCompoundChildren ? children : children && <CardContent>{children}</CardContent>}
91
+ {footer && <CardFooter>{footer}</CardFooter>}
92
+ </div>
93
+ );
94
+ }
95
+
96
+ function CardHeader({ ...props }: Omit<React.ComponentProps<'div'>, 'className'>) {
97
+ return (
98
+ <div
99
+ data-slot="card-header"
100
+ className="gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3 group/card-header @container/card-header grid auto-rows-min items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]"
101
+ {...props}
102
+ />
103
+ );
104
+ }
105
+
106
+ function CardTitle({ ...props }: Omit<React.ComponentProps<'div'>, 'className'>) {
107
+ return (
108
+ <div
109
+ data-slot="card-title"
110
+ className="text-base leading-snug font-medium group-data-[size=sm]/card:text-sm"
111
+ {...props}
112
+ />
113
+ );
114
+ }
115
+
116
+ function CardDescription({ ...props }: Omit<React.ComponentProps<'div'>, 'className'>) {
117
+ return <div data-slot="card-description" className="text-muted-foreground text-sm" {...props} />;
118
+ }
119
+
120
+ function CardAction({ ...props }: Omit<React.ComponentProps<'div'>, 'className'>) {
121
+ return (
122
+ <div
123
+ data-slot="card-action"
124
+ className="col-start-2 row-span-2 row-start-1 self-start justify-self-end"
125
+ {...props}
126
+ />
127
+ );
128
+ }
129
+
130
+ function CardContent({ ...props }: Omit<React.ComponentProps<'div'>, 'className'>) {
131
+ return (
132
+ <div data-slot="card-content" className="px-4 group-data-[size=sm]/card:px-3" {...props} />
133
+ );
134
+ }
135
+
136
+ function CardFooter({ ...props }: Omit<React.ComponentProps<'div'>, 'className'>) {
137
+ return (
138
+ <div
139
+ data-slot="card-footer"
140
+ className="bg-muted/50 rounded-b-xl border-t p-4 group-data-[size=sm]/card:p-3 flex items-center"
141
+ {...props}
142
+ />
143
+ );
144
+ }
145
+
146
+ export {
147
+ Card,
148
+ CardAction,
149
+ CardContent,
150
+ CardDescription,
151
+ CardFooter,
152
+ CardHeader,
153
+ CardTitle,
154
+ cardVariants,
155
+ };
@@ -0,0 +1,216 @@
1
+ import useEmblaCarousel, { type UseEmblaCarouselType } from 'embla-carousel-react';
2
+ import * as React from 'react';
3
+
4
+ import { Button as ButtonPrimitive } from '@base-ui/react/button';
5
+ import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react';
6
+ import { cn } from '../../../lib/utils';
7
+ import { buttonVariants } from './button';
8
+
9
+ type CarouselApi = UseEmblaCarouselType[1];
10
+ type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
11
+ type CarouselOptions = UseCarouselParameters[0];
12
+ type CarouselPlugin = UseCarouselParameters[1];
13
+
14
+ type CarouselProps = {
15
+ opts?: CarouselOptions;
16
+ plugins?: CarouselPlugin;
17
+ orientation?: 'horizontal' | 'vertical';
18
+ setApi?: (api: CarouselApi) => void;
19
+ };
20
+
21
+ type CarouselContextProps = {
22
+ carouselRef: ReturnType<typeof useEmblaCarousel>[0];
23
+ api: ReturnType<typeof useEmblaCarousel>[1];
24
+ scrollPrev: () => void;
25
+ scrollNext: () => void;
26
+ canScrollPrev: boolean;
27
+ canScrollNext: boolean;
28
+ } & CarouselProps;
29
+
30
+ const CarouselContext = React.createContext<CarouselContextProps | null>(null);
31
+
32
+ function useCarousel() {
33
+ const context = React.useContext(CarouselContext);
34
+
35
+ if (!context) {
36
+ throw new Error('useCarousel must be used within a <Carousel />');
37
+ }
38
+
39
+ return context;
40
+ }
41
+
42
+ function Carousel({
43
+ orientation = 'horizontal',
44
+ opts,
45
+ setApi,
46
+ plugins,
47
+ className,
48
+ children,
49
+ ...props
50
+ }: React.ComponentProps<'div'> & CarouselProps) {
51
+ const [carouselRef, api] = useEmblaCarousel(
52
+ {
53
+ ...opts,
54
+ axis: orientation === 'horizontal' ? 'x' : 'y',
55
+ },
56
+ plugins,
57
+ );
58
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false);
59
+ const [canScrollNext, setCanScrollNext] = React.useState(false);
60
+
61
+ const onSelect = React.useCallback((api: CarouselApi) => {
62
+ if (!api) return;
63
+ setCanScrollPrev(api.canScrollPrev());
64
+ setCanScrollNext(api.canScrollNext());
65
+ }, []);
66
+
67
+ const scrollPrev = React.useCallback(() => {
68
+ api?.scrollPrev();
69
+ }, [api]);
70
+
71
+ const scrollNext = React.useCallback(() => {
72
+ api?.scrollNext();
73
+ }, [api]);
74
+
75
+ const handleKeyDown = React.useCallback(
76
+ (event: React.KeyboardEvent<HTMLDivElement>) => {
77
+ if (event.key === 'ArrowLeft') {
78
+ event.preventDefault();
79
+ scrollPrev();
80
+ } else if (event.key === 'ArrowRight') {
81
+ event.preventDefault();
82
+ scrollNext();
83
+ }
84
+ },
85
+ [scrollPrev, scrollNext],
86
+ );
87
+
88
+ React.useEffect(() => {
89
+ if (!api || !setApi) return;
90
+ setApi(api);
91
+ }, [api, setApi]);
92
+
93
+ React.useEffect(() => {
94
+ if (!api) return;
95
+ onSelect(api);
96
+ api.on('reInit', onSelect);
97
+ api.on('select', onSelect);
98
+
99
+ return () => {
100
+ api?.off('select', onSelect);
101
+ };
102
+ }, [api, onSelect]);
103
+
104
+ return (
105
+ <CarouselContext.Provider
106
+ value={{
107
+ carouselRef,
108
+ api: api,
109
+ opts,
110
+ orientation: orientation || (opts?.axis === 'y' ? 'vertical' : 'horizontal'),
111
+ scrollPrev,
112
+ scrollNext,
113
+ canScrollPrev,
114
+ canScrollNext,
115
+ }}
116
+ >
117
+ <div
118
+ onKeyDownCapture={handleKeyDown}
119
+ className={cn('relative', className)}
120
+ role="region"
121
+ aria-roledescription="carousel"
122
+ data-slot="carousel"
123
+ {...props}
124
+ >
125
+ {children}
126
+ </div>
127
+ </CarouselContext.Provider>
128
+ );
129
+ }
130
+
131
+ function CarouselContent({ className, ...props }: React.ComponentProps<'div'>) {
132
+ const { carouselRef, orientation } = useCarousel();
133
+
134
+ return (
135
+ <div ref={carouselRef} className="overflow-hidden" data-slot="carousel-content">
136
+ <div
137
+ className={cn('flex', orientation === 'horizontal' ? '-ml-4' : '-mt-4 flex-col', className)}
138
+ {...props}
139
+ />
140
+ </div>
141
+ );
142
+ }
143
+
144
+ function CarouselItem({ className, ...props }: React.ComponentProps<'div'>) {
145
+ const { orientation } = useCarousel();
146
+
147
+ return (
148
+ <div
149
+ role="group"
150
+ aria-roledescription="slide"
151
+ data-slot="carousel-item"
152
+ className={cn(
153
+ 'min-w-0 shrink-0 grow-0 basis-full',
154
+ orientation === 'horizontal' ? 'pl-4' : 'pt-4',
155
+ className,
156
+ )}
157
+ {...props}
158
+ />
159
+ );
160
+ }
161
+
162
+ function CarouselPrevious({ ...props }: Omit<ButtonPrimitive.Props, 'className'>) {
163
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel();
164
+
165
+ return (
166
+ <ButtonPrimitive
167
+ data-slot="carousel-previous"
168
+ className={cn(
169
+ buttonVariants({ variant: 'outline', size: 'icon-sm' }),
170
+ 'rounded-full absolute touch-manipulation',
171
+ orientation === 'horizontal'
172
+ ? 'top-1/2 -left-12 -translate-y-1/2'
173
+ : '-top-12 left-1/2 -translate-x-1/2 rotate-90',
174
+ )}
175
+ disabled={!canScrollPrev}
176
+ onClick={scrollPrev}
177
+ {...props}
178
+ >
179
+ <ChevronLeftIcon />
180
+ <span className="sr-only">Previous slide</span>
181
+ </ButtonPrimitive>
182
+ );
183
+ }
184
+
185
+ function CarouselNext({ ...props }: Omit<ButtonPrimitive.Props, 'className'>) {
186
+ const { orientation, scrollNext, canScrollNext } = useCarousel();
187
+
188
+ return (
189
+ <ButtonPrimitive
190
+ data-slot="carousel-next"
191
+ className={cn(
192
+ buttonVariants({ variant: 'outline', size: 'icon-sm' }),
193
+ 'rounded-full absolute touch-manipulation',
194
+ orientation === 'horizontal'
195
+ ? 'top-1/2 -right-12 -translate-y-1/2'
196
+ : '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',
197
+ )}
198
+ disabled={!canScrollNext}
199
+ onClick={scrollNext}
200
+ {...props}
201
+ >
202
+ <ChevronRightIcon />
203
+ <span className="sr-only">Next slide</span>
204
+ </ButtonPrimitive>
205
+ );
206
+ }
207
+
208
+ export {
209
+ Carousel,
210
+ CarouselContent,
211
+ CarouselItem,
212
+ CarouselNext,
213
+ CarouselPrevious,
214
+ useCarousel,
215
+ type CarouselApi,
216
+ };