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