carousel.tsx 6.1 KB

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