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