carousel.tsx 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  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"
  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. function Carousel({
  34. orientation = "horizontal",
  35. opts,
  36. setApi,
  37. plugins,
  38. className,
  39. children,
  40. ...props
  41. }: React.ComponentProps<"div"> & CarouselProps) {
  42. const [carouselRef, api] = useEmblaCarousel(
  43. {
  44. ...opts,
  45. axis: orientation === "horizontal" ? "x" : "y",
  46. },
  47. plugins
  48. )
  49. const [canScrollPrev, setCanScrollPrev] = React.useState(false)
  50. const [canScrollNext, setCanScrollNext] = React.useState(false)
  51. const onSelect = React.useCallback((api: CarouselApi) => {
  52. if (!api) return
  53. setCanScrollPrev(api.canScrollPrev())
  54. setCanScrollNext(api.canScrollNext())
  55. }, [])
  56. const scrollPrev = React.useCallback(() => {
  57. api?.scrollPrev()
  58. }, [api])
  59. const scrollNext = React.useCallback(() => {
  60. api?.scrollNext()
  61. }, [api])
  62. const handleKeyDown = React.useCallback(
  63. (event: React.KeyboardEvent<HTMLDivElement>) => {
  64. if (event.key === "ArrowLeft") {
  65. event.preventDefault()
  66. scrollPrev()
  67. } else if (event.key === "ArrowRight") {
  68. event.preventDefault()
  69. scrollNext()
  70. }
  71. },
  72. [scrollPrev, scrollNext]
  73. )
  74. React.useEffect(() => {
  75. if (!api || !setApi) return
  76. setApi(api)
  77. }, [api, setApi])
  78. React.useEffect(() => {
  79. if (!api) return
  80. onSelect(api)
  81. api.on("reInit", onSelect)
  82. api.on("select", onSelect)
  83. return () => {
  84. api?.off("select", onSelect)
  85. }
  86. }, [api, onSelect])
  87. return (
  88. <CarouselContext.Provider
  89. value={{
  90. carouselRef,
  91. api: api,
  92. opts,
  93. orientation:
  94. orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
  95. scrollPrev,
  96. scrollNext,
  97. canScrollPrev,
  98. canScrollNext,
  99. }}
  100. >
  101. <div
  102. onKeyDownCapture={handleKeyDown}
  103. className={cn("relative", className)}
  104. role="region"
  105. aria-roledescription="carousel"
  106. data-slot="carousel"
  107. {...props}
  108. >
  109. {children}
  110. </div>
  111. </CarouselContext.Provider>
  112. )
  113. }
  114. function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
  115. const { carouselRef, orientation } = useCarousel()
  116. return (
  117. <div
  118. ref={carouselRef}
  119. className="overflow-hidden"
  120. data-slot="carousel-content"
  121. >
  122. <div
  123. className={cn(
  124. "flex",
  125. orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
  126. className
  127. )}
  128. {...props}
  129. />
  130. </div>
  131. )
  132. }
  133. function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
  134. const { orientation } = useCarousel()
  135. return (
  136. <div
  137. role="group"
  138. aria-roledescription="slide"
  139. data-slot="carousel-item"
  140. className={cn(
  141. "min-w-0 shrink-0 grow-0 basis-full",
  142. orientation === "horizontal" ? "pl-4" : "pt-4",
  143. className
  144. )}
  145. {...props}
  146. />
  147. )
  148. }
  149. function CarouselPrevious({
  150. className,
  151. variant = "outline",
  152. size = "icon",
  153. ...props
  154. }: React.ComponentProps<typeof Button>) {
  155. const { orientation, scrollPrev, canScrollPrev } = useCarousel()
  156. return (
  157. <Button
  158. data-slot="carousel-previous"
  159. variant={variant}
  160. size={size}
  161. className={cn(
  162. "absolute size-8 rounded-full",
  163. orientation === "horizontal"
  164. ? "top-1/2 -left-12 -translate-y-1/2"
  165. : "-top-12 left-1/2 -translate-x-1/2 rotate-90",
  166. className
  167. )}
  168. disabled={!canScrollPrev}
  169. onClick={scrollPrev}
  170. {...props}
  171. >
  172. <ArrowLeft />
  173. <span className="sr-only">Previous slide</span>
  174. </Button>
  175. )
  176. }
  177. function CarouselNext({
  178. className,
  179. variant = "outline",
  180. size = "icon",
  181. ...props
  182. }: React.ComponentProps<typeof Button>) {
  183. const { orientation, scrollNext, canScrollNext } = useCarousel()
  184. return (
  185. <Button
  186. data-slot="carousel-next"
  187. variant={variant}
  188. size={size}
  189. className={cn(
  190. "absolute size-8 rounded-full",
  191. orientation === "horizontal"
  192. ? "top-1/2 -right-12 -translate-y-1/2"
  193. : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
  194. className
  195. )}
  196. disabled={!canScrollNext}
  197. onClick={scrollNext}
  198. {...props}
  199. >
  200. <ArrowRight />
  201. <span className="sr-only">Next slide</span>
  202. </Button>
  203. )
  204. }
  205. export {
  206. type CarouselApi,
  207. Carousel,
  208. CarouselContent,
  209. CarouselItem,
  210. CarouselPrevious,
  211. CarouselNext,
  212. }