carousel.tsx 5.5 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. };