Sunflower UI
Search
Copy Code
Light Mode
Mountain Peaks

Mountain Peaks

Swiss Alps, Switzerland

Nankumba Peninsula

Nankumba Peninsula

Lake Malawi, Malawi

Coastal Retreat

Coastal Retreat

Amalfi Coast, Italy

Desert Dunes

Desert Dunes

Sahara, Morocco

Tropical Bay

Tropical Bay

Phuket, Thailand

carousel.tsx
globals.css
prompt.md
Collapse
Reset
Copy Code
Download
Run
1
'use client';
2
3
import * as React from 'react';
4
import useEmblaCarousel, { type UseEmblaCarouselType } from 'embla-carousel-react';
5
import { ChevronLeft, ChevronRight } from 'lucide-react';
6
7
import { cn } from '@/lib/utils';
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
export 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
const Carousel = React.forwardRef<
43
HTMLDivElement,
44
React.HTMLAttributes<HTMLDivElement> & CarouselProps
45
>(
46
(
47
{
48
orientation = 'horizontal',
49
opts,
50
setApi,
51
plugins,
52
className,
53
children,
54
...props
55
},
56
ref
57
) => {
58
const [carouselRef, api] = useEmblaCarousel(
59
{
60
...opts,
61
axis: orientation === 'horizontal' ? 'x' : 'y',
62
},
63
plugins
64
);
65
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
66
const [canScrollNext, setCanScrollNext] = React.useState(false);
67
68
const onSelect = React.useCallback((api: CarouselApi) => {
69
if (!api) return;
70
setCanScrollPrev(api.canScrollPrev());
71
setCanScrollNext(api.canScrollNext());
72
}, []);
73
74
const scrollPrev = React.useCallback(() => {
75
api?.scrollPrev();
76
}, [api]);
77
78
const scrollNext = React.useCallback(() => {
79
api?.scrollNext();
80
}, [api]);
81
82
const handleKeyDown = React.useCallback(
83
(event: React.KeyboardEvent<HTMLDivElement>) => {
84
if (event.defaultPrevented) return;
85
const target = event.target as HTMLElement | null;
86
if (
87
target?.isContentEditable ||
88
target?.closest(
89
'input, textarea, select, [contenteditable="true"], [role="slider"]'
90
)
91
) {
92
return;
93
}
94
const back = orientation === 'vertical' ? 'ArrowUp' : 'ArrowLeft';
95
const forward = orientation === 'vertical' ? 'ArrowDown' : 'ArrowRight';
96
if (event.key === back) {
97
event.preventDefault();
98
scrollPrev();
99
} else if (event.key === forward) {
100
event.preventDefault();
101
scrollNext();
102
}
103
},
104
[orientation, scrollPrev, scrollNext]
105
);
106
107
React.useEffect(() => {
108
if (!api || !setApi) return;
109
setApi(api);
110
}, [api, setApi]);
111
112
React.useEffect(() => {
113
if (!api) return;
114
onSelect(api);
115
api.on('reInit', onSelect);
116
api.on('select', onSelect);
117
118
return () => {
119
api?.off('select', onSelect);
120
};
121
}, [api, onSelect]);
122
123
return (
124
<CarouselContext.Provider
125
value={{
126
carouselRef,
127
api,
128
opts,
129
orientation:
130
orientation || (opts?.axis === 'y' ? 'vertical' : 'horizontal'),
131
scrollPrev,
132
scrollNext,
133
canScrollPrev,
134
canScrollNext,
135
}}
136
>
137
<div
138
ref={ref}
139
tabIndex={0}
140
onKeyDown={handleKeyDown}
141
className={cn(
142
'relative outline-none',
143
className
144
)}
145
role="region"
146
aria-roledescription="carousel"
147
{...props}
148
>
149
{children}
150
</div>
151
</CarouselContext.Provider>
152
);
153
}
154
);
155
Carousel.displayName = 'Carousel';
156
157
const CarouselContent = React.forwardRef<
158
HTMLDivElement,
159
React.HTMLAttributes<HTMLDivElement>
160
>(({ className, ...props }, ref) => {
161
const { carouselRef, orientation } = useCarousel();
162
163
return (
164
<div ref={carouselRef} className="overflow-hidden">
165
<div
166
ref={ref}
167
className={cn(
168
'flex',
169
orientation === 'horizontal' ? '-ml-4' : '-mt-4 flex-col',
170
className
171
)}
172
{...props}
173
/>
174
</div>
175
);
176
});
177
CarouselContent.displayName = 'CarouselContent';
178
179
const CarouselItem = React.forwardRef<
180
HTMLDivElement,
181
React.HTMLAttributes<HTMLDivElement>
182
>(({ className, ...props }, ref) => {
183
const { orientation } = useCarousel();
184
185
return (
186
<div
187
ref={ref}
188
role="group"
189
aria-roledescription="slide"
190
className={cn(
191
'min-w-0 shrink-0 grow-0 basis-full',
192
orientation === 'horizontal' ? 'pl-4' : 'pt-4',
193
className
194
)}
195
{...props}
196
/>
197
);
198
});
199
CarouselItem.displayName = 'CarouselItem';
200
201
const CarouselPrevious = React.forwardRef<
202
HTMLButtonElement,
203
React.ComponentProps<'button'>
204
>(({ className, ...props }, ref) => {
205
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
206
207
return (
208
<button
209
ref={ref}
210
type="button"
211
className={cn(
212
'absolute h-10 w-10 rounded-full bg-white/70 dark:bg-black/60 backdrop-blur-[16px] border border-black/5 dark:border-white/5 shadow-[0_8px_32px_rgba(0,0,0,0.05)] dark:shadow-none text-zinc-950 dark:text-zinc-50 hover:bg-white dark:hover:bg-zinc-800 transition-all duration-500 ease-[cubic-bezier(0.23,1,0.32,1)]',
213
orientation === 'horizontal'
214
? '-left-14 top-1/2 -translate-y-1/2'
215
: '-top-14 left-1/2 -translate-x-1/2 rotate-90',
216
className
217
)}
218
disabled={!canScrollPrev}
219
onClick={scrollPrev}
220
{...props}
221
>
222
<ChevronLeft className="h-5 w-5" />
223
<span className="sr-only">Previous slide</span>
224
</button>
225
);
226
});
227
CarouselPrevious.displayName = 'CarouselPrevious';
228
229
const CarouselNext = React.forwardRef<
230
HTMLButtonElement,
231
React.ComponentProps<'button'>
232
>(({ className, ...props }, ref) => {
233
const { orientation, scrollNext, canScrollNext } = useCarousel();
234
235
return (
236
<button
237
ref={ref}
238
type="button"
239
className={cn(
240
'absolute h-10 w-10 rounded-full bg-white/70 dark:bg-black/60 backdrop-blur-[16px] border border-black/5 dark:border-white/5 shadow-[0_8px_32px_rgba(0,0,0,0.05)] dark:shadow-none text-zinc-950 dark:text-zinc-50 hover:bg-white dark:hover:bg-zinc-800 transition-all duration-500 ease-[cubic-bezier(0.23,1,0.32,1)]',
241
orientation === 'horizontal'
242
? '-right-14 top-1/2 -translate-y-1/2'
243
: '-bottom-14 left-1/2 -translate-x-1/2 rotate-90',
244
className
245
)}
246
disabled={!canScrollNext}
247
onClick={scrollNext}
248
{...props}
249
>
250
<ChevronRight className="h-5 w-5" />
251
<span className="sr-only">Next slide</span>
252
</button>
253
);
254
});
255
CarouselNext.displayName = 'CarouselNext';
256
257
export {
258
type CarouselApi,
259
Carousel,
260
CarouselContent,
261
CarouselItem,
262
CarouselPrevious,
263
CarouselNext,
264
};
265