1import * as React from "react"
2import useEmblaCarousel, {
3 type UseEmblaCarouselType,
4} from "embla-carousel-react"
5import { ArrowLeft, ArrowRight } from "lucide-react"
6
7import { cn } from "@/lib/utils"
8import { Button } from "@/components/ui/button"
9
10type CarouselApi = UseEmblaCarouselType[1]
11type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
12type CarouselOptions = UseCarouselParameters[0]
13type CarouselPlugin = UseCarouselParameters[1]
14
15type CarouselProps = {
16 opts?: CarouselOptions
17 plugins?: CarouselPlugin
18 orientation?: "horizontal" | "vertical"
19 setApi?: (api: CarouselApi) => void
20}
21
22type CarouselContextProps = {
23 carouselRef: ReturnType<typeof useEmblaCarousel>[0]
24 api: ReturnType<typeof useEmblaCarousel>[1]
25 scrollPrev: () => void
26 scrollNext: () => void
27 canScrollPrev: boolean
28 canScrollNext: boolean
29} & CarouselProps
30
31const CarouselContext = React.createContext<CarouselContextProps | null>(null)
32
33function useCarousel() {
34 const context = React.useContext(CarouselContext)
35
36 if (!context) {
37 throw new Error("useCarousel must be used within a <Carousel />")
38 }
39
40 return context
41}
42
43const Carousel = React.forwardRef<
44 HTMLDivElement,
45 React.HTMLAttributes<HTMLDivElement> & CarouselProps
46>(
47 (
48 {
49 orientation = "horizontal",
50 opts,
51 setApi,
52 plugins,
53 className,
54 children,
55 ...props
56 },
57 ref
58 ) => {
59 const [carouselRef, api] = useEmblaCarousel(
60 {
61 ...opts,
62 axis: orientation === "horizontal" ? "x" : "y",
63 },
64 plugins
65 )
66 const [canScrollPrev, setCanScrollPrev] = React.useState(false)
67 const [canScrollNext, setCanScrollNext] = React.useState(false)
68
69 const onSelect = React.useCallback((api: CarouselApi) => {
70 if (!api) {
71 return
72 }
73
74 setCanScrollPrev(api.canScrollPrev())
75 setCanScrollNext(api.canScrollNext())
76 }, [])
77
78 const scrollPrev = React.useCallback(() => {
79 api?.scrollPrev()
80 }, [api])
81
82 const scrollNext = React.useCallback(() => {
83 api?.scrollNext()
84 }, [api])
85
86 const handleKeyDown = React.useCallback(
87 (event: React.KeyboardEvent<HTMLDivElement>) => {
88 if (event.key === "ArrowLeft") {
89 event.preventDefault()
90 scrollPrev()
91 } else if (event.key === "ArrowRight") {
92 event.preventDefault()
93 scrollNext()
94 }
95 },
96 [scrollPrev, scrollNext]
97 )
98
99 React.useEffect(() => {
100 if (!api || !setApi) {
101 return
102 }
103
104 setApi(api)
105 }, [api, setApi])
106
107 React.useEffect(() => {
108 if (!api) {
109 return
110 }
111
112 onSelect(api)
113 api.on("reInit", onSelect)
114 api.on("select", onSelect)
115
116 return () => {
117 api?.off("select", onSelect)
118 }
119 }, [api, onSelect])
120
121 return (
122 <CarouselContext.Provider
123 value={{
124 carouselRef,
125 api: api,
126 opts,
127 orientation:
128 orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
129 scrollPrev,
130 scrollNext,
131 canScrollPrev,
132 canScrollNext,
133 }}
134 >
135 <div
136 ref={ref}
137 onKeyDownCapture={handleKeyDown}
138 className={cn("relative", className)}
139 role="region"
140 aria-roledescription="carousel"
141 {...props}
142 >
143 {children}
144 </div>
145 </CarouselContext.Provider>
146 )
147 }
148)
149Carousel.displayName = "Carousel"
150
151const CarouselContent = React.forwardRef<
152 HTMLDivElement,
153 React.HTMLAttributes<HTMLDivElement>
154>(({ className, ...props }, ref) => {
155 const { carouselRef, orientation } = useCarousel()
156
157 return (
158 <div ref={carouselRef} className="overflow-hidden">
159 <div
160 ref={ref}
161 className={cn(
162 "flex",
163 orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
164 className
165 )}
166 {...props}
167 />
168 </div>
169 )
170})
171CarouselContent.displayName = "CarouselContent"
172
173const CarouselItem = React.forwardRef<
174 HTMLDivElement,
175 React.HTMLAttributes<HTMLDivElement>
176>(({ className, ...props }, ref) => {
177 const { orientation } = useCarousel()
178
179 return (
180 <div
181 ref={ref}
182 role="group"
183 aria-roledescription="slide"
184 className={cn(
185 "min-w-0 shrink-0 grow-0 basis-full",
186 orientation === "horizontal" ? "pl-4" : "pt-4",
187 className
188 )}
189 {...props}
190 />
191 )
192})
193CarouselItem.displayName = "CarouselItem"
194
195const CarouselPrevious = React.forwardRef<
196 HTMLButtonElement,
197 React.ComponentProps<typeof Button>
198>(({ className, variant = "outline-solid", size = "icon", ...props }, ref) => {
199 const { orientation, scrollPrev, canScrollPrev } = useCarousel()
200
201 return (
202 <Button
203 ref={ref}
204 variant={variant}
205 size={size}
206 className={cn(
207 "absolute h-8 w-8 rounded-full",
208 orientation === "horizontal"
209 ? "-left-12 top-1/2 -translate-y-1/2"
210 : "-top-12 left-1/2 -translate-x-1/2 rotate-90",
211 className
212 )}
213 disabled={!canScrollPrev}
214 onClick={scrollPrev}
215 {...props}
216 >
217 <ArrowLeft className="h-4 w-4" />
218 <span className="sr-only">Previous slide</span>
219 </Button>
220 )
221})
222CarouselPrevious.displayName = "CarouselPrevious"
223
224const CarouselNext = React.forwardRef<
225 HTMLButtonElement,
226 React.ComponentProps<typeof Button>
227>(({ className, variant = "outline-solid", size = "icon", ...props }, ref) => {
228 const { orientation, scrollNext, canScrollNext } = useCarousel()
229
230 return (
231 <Button
232 ref={ref}
233 variant={variant}
234 size={size}
235 className={cn(
236 "absolute h-8 w-8 rounded-full",
237 orientation === "horizontal"
238 ? "-right-12 top-1/2 -translate-y-1/2"
239 : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
240 className
241 )}
242 disabled={!canScrollNext}
243 onClick={scrollNext}
244 {...props}
245 >
246 <ArrowRight className="h-4 w-4" />
247 <span className="sr-only">Next slide</span>
248 </Button>
249 )
250})
251CarouselNext.displayName = "CarouselNext"
252
253export {
254 type CarouselApi,
255 Carousel,
256 CarouselContent,
257 CarouselItem,
258 CarouselPrevious,
259 CarouselNext,
260}