carousel.tsx 5.4 KB

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