carousel.tsx 6.1 KB

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