{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "carousel", "title": "Carousel", "description": "Horizontally scrollable content carousel with navigation controls for browsing items.", "dependencies": [ "embla-carousel-react", "embla-carousel-wheel-gestures", "lucide-react" ], "registryDependencies": [ "button" ], "files": [ { "path": "packages/ui/src/carousel.tsx", "content": "\"use client\";\n\nimport useEmblaCarousel, {\n type UseEmblaCarouselType,\n} from \"embla-carousel-react\";\nimport {\n ChevronLeft as ChevronLeftIcon,\n ChevronRight as ChevronRightIcon,\n} from \"lucide-react\";\nimport * as React from \"react\";\nimport { Button } from \"./button\";\nimport { cn } from \"./utils\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\ntype CarouselApi = UseEmblaCarouselType[1];\ntype UseCarouselParameters = Parameters;\ntype CarouselOptions = UseCarouselParameters[0];\ntype CarouselPlugin = UseCarouselParameters[1];\n\ninterface CarouselProps {\n /**\n * When true, prev/next buttons are hidden by default and revealed on hover.\n * Arrow components read this from context and apply the appropriate classes.\n */\n autoHideArrows?: boolean;\n /**\n * Gap between slides. Accepts any CSS length value.\n * Set as `--carousel-gap` custom property on the root element.\n * @default \"1rem\"\n */\n gap?: string;\n /** Callback fired with the selected snap index when the active slide changes */\n onSlideChange?: (index: number) => void;\n opts?: CarouselOptions;\n orientation?: \"horizontal\" | \"vertical\";\n plugins?: CarouselPlugin;\n setApi?: (api: CarouselApi) => void;\n}\n\ntype CarouselContextProps = {\n carouselRef: ReturnType[0];\n api: ReturnType[1];\n scrollPrev: () => void;\n scrollNext: () => void;\n canScrollPrev: boolean;\n canScrollNext: boolean;\n selectedIndex: number;\n slideCount: number;\n autoHideArrows: boolean;\n} & Pick;\n\n// ---------------------------------------------------------------------------\n// Context\n// ---------------------------------------------------------------------------\n\nconst CarouselContext = React.createContext(null);\n\nfunction useCarousel() {\n const context = React.useContext(CarouselContext);\n if (!context) {\n throw new Error(\"useCarousel must be used within a \");\n }\n return context;\n}\n\n// ---------------------------------------------------------------------------\n// Carousel (root)\n// ---------------------------------------------------------------------------\n\nfunction Carousel({\n orientation = \"horizontal\",\n opts,\n setApi,\n plugins,\n onSlideChange,\n autoHideArrows = false,\n gap = \"1rem\",\n className,\n children,\n style,\n ...props\n}: React.ComponentProps<\"div\"> & CarouselProps) {\n const [carouselRef, api] = useEmblaCarousel(\n {\n ...opts,\n axis: orientation === \"horizontal\" ? \"x\" : \"y\",\n },\n plugins\n );\n\n const [canScrollPrev, setCanScrollPrev] = React.useState(false);\n const [canScrollNext, setCanScrollNext] = React.useState(false);\n const [selectedIndex, setSelectedIndex] = React.useState(0);\n const [slideCount, setSlideCount] = React.useState(0);\n\n const onSelect = React.useCallback(\n (emblaApi: CarouselApi) => {\n if (!emblaApi) {\n return;\n }\n const index = emblaApi.selectedScrollSnap();\n setSelectedIndex(index);\n setCanScrollPrev(emblaApi.canScrollPrev());\n setCanScrollNext(emblaApi.canScrollNext());\n onSlideChange?.(index);\n },\n [onSlideChange]\n );\n\n const scrollPrev = React.useCallback(() => {\n api?.scrollPrev();\n }, [api]);\n\n const scrollNext = React.useCallback(() => {\n api?.scrollNext();\n }, [api]);\n\n const handleKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (event.key === \"ArrowLeft\") {\n event.preventDefault();\n scrollPrev();\n } else if (event.key === \"ArrowRight\") {\n event.preventDefault();\n scrollNext();\n }\n },\n [scrollPrev, scrollNext]\n );\n\n React.useEffect(() => {\n if (!(api && setApi)) {\n return;\n }\n setApi(api);\n }, [api, setApi]);\n\n React.useEffect(() => {\n if (!api) {\n return;\n }\n\n setSlideCount(api.scrollSnapList().length);\n onSelect(api);\n\n api.on(\"reInit\", onSelect);\n api.on(\"select\", onSelect);\n api.on(\"reInit\", () => setSlideCount(api.scrollSnapList().length));\n\n return () => {\n api.off(\"select\", onSelect);\n api.off(\"reInit\", onSelect);\n };\n }, [api, onSelect]);\n\n return (\n \n \n {children}\n \n \n );\n}\n\n// ---------------------------------------------------------------------------\n// CarouselContent\n// ---------------------------------------------------------------------------\n\nfunction CarouselContent({\n className,\n mask = false,\n style,\n ...props\n}: React.ComponentProps<\"div\"> & {\n /**\n * When true, applies a CSS `mask-image` fade on edges that have\n * more content to scroll to. The fade width is controlled by the\n * `--carousel-mask-width` CSS variable (default `4rem`).\n */\n mask?: boolean;\n}) {\n const { carouselRef, orientation, canScrollPrev, canScrollNext } =\n useCarousel();\n\n const maskStyle = React.useMemo(() => {\n if (!mask) {\n return;\n }\n const w = \"var(--carousel-mask-width, 4rem)\";\n const dir = orientation === \"horizontal\" ? \"to right\" : \"to bottom\";\n let value: string | undefined;\n\n if (canScrollPrev && canScrollNext) {\n value = `linear-gradient(${dir}, transparent, black ${w}, black calc(100% - ${w}), transparent)`;\n } else if (canScrollPrev) {\n value = `linear-gradient(${dir}, transparent, black ${w})`;\n } else if (canScrollNext) {\n value = `linear-gradient(${dir}, black calc(100% - ${w}), transparent)`;\n }\n\n if (!value) {\n return;\n }\n return {\n maskImage: value,\n WebkitMaskImage: value,\n };\n }, [mask, orientation, canScrollPrev, canScrollNext]);\n\n return (\n \n \n \n );\n}\n\n// ---------------------------------------------------------------------------\n// CarouselItem\n// ---------------------------------------------------------------------------\n\nfunction CarouselItem({ className, ...props }: React.ComponentProps<\"div\">) {\n const { orientation } = useCarousel();\n\n return (\n \n );\n}\n\n// ---------------------------------------------------------------------------\n// CarouselPrevious\n// ---------------------------------------------------------------------------\n\nfunction CarouselPrevious({\n className,\n variant = \"outline\",\n size = \"icon-xs\",\n ...props\n}: React.ComponentProps) {\n const { orientation, scrollPrev, canScrollPrev, autoHideArrows } =\n useCarousel();\n\n return (\n \n \n Previous slide\n \n );\n}\n\n// ---------------------------------------------------------------------------\n// CarouselNext\n// ---------------------------------------------------------------------------\n\nfunction CarouselNext({\n className,\n variant = \"outline\",\n size = \"icon-xs\",\n ...props\n}: React.ComponentProps) {\n const { orientation, scrollNext, canScrollNext, autoHideArrows } =\n useCarousel();\n\n return (\n \n \n Next slide\n \n );\n}\n\n// ---------------------------------------------------------------------------\n// CarouselDots\n// ---------------------------------------------------------------------------\n\nfunction CarouselDots({ className, ...props }: React.ComponentProps<\"div\">) {\n const { api, selectedIndex, slideCount } = useCarousel();\n\n if (slideCount <= 1) {\n return null;\n }\n\n return (\n \n {Array.from({ length: slideCount }).map((_, index) => (\n api?.scrollTo(index)}\n type=\"button\"\n />\n ))}\n \n );\n}\n\n// ---------------------------------------------------------------------------\n// CarouselCounter\n// ---------------------------------------------------------------------------\n\nfunction CarouselCounter({ className, ...props }: React.ComponentProps<\"div\">) {\n const { selectedIndex, slideCount } = useCarousel();\n\n if (slideCount <= 1) {\n return null;\n }\n\n return (\n \n {selectedIndex + 1} / {slideCount}\n \n );\n}\n\n// ---------------------------------------------------------------------------\n// Exports\n// ---------------------------------------------------------------------------\n\nexport {\n Carousel,\n type CarouselApi,\n CarouselContent,\n CarouselCounter,\n CarouselDots,\n CarouselItem,\n CarouselNext,\n type CarouselOptions,\n type CarouselPlugin,\n CarouselPrevious,\n useCarousel,\n};\n", "type": "registry:ui" } ], "type": "registry:ui" }