| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231 |
- 'use client';
- import * as React from 'react';
- import useEmblaCarousel, { type UseEmblaCarouselType } from 'embla-carousel-react';
- import { cn } from '@/lib/utils';
- import { Button } from '@/components/ui/button';
- import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react';
- type CarouselApi = UseEmblaCarouselType[1];
- type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
- type CarouselOptions = UseCarouselParameters[0];
- type CarouselPlugin = UseCarouselParameters[1];
- type CarouselProps = {
- opts?: CarouselOptions;
- plugins?: CarouselPlugin;
- orientation?: 'horizontal' | 'vertical';
- setApi?: (api: CarouselApi) => void;
- };
- type CarouselContextProps = {
- carouselRef: ReturnType<typeof useEmblaCarousel>[0];
- api: ReturnType<typeof useEmblaCarousel>[1];
- scrollPrev: () => void;
- scrollNext: () => void;
- canScrollPrev: boolean;
- canScrollNext: boolean;
- } & CarouselProps;
- const CarouselContext = React.createContext<CarouselContextProps | null>(null);
- function useCarousel() {
- const context = React.useContext(CarouselContext);
- if (!context) {
- throw new Error('useCarousel must be used within a <Carousel />');
- }
- return context;
- }
- function Carousel({
- orientation = 'horizontal',
- opts,
- setApi,
- plugins,
- className,
- children,
- ...props
- }: React.ComponentProps<'div'> & CarouselProps) {
- const [carouselRef, api] = useEmblaCarousel(
- {
- ...opts,
- axis: orientation === 'horizontal' ? 'x' : 'y',
- },
- plugins,
- );
- const [canScrollPrev, setCanScrollPrev] = React.useState(false);
- const [canScrollNext, setCanScrollNext] = React.useState(false);
- const onSelect = React.useCallback((api: CarouselApi) => {
- if (!api) return;
- setCanScrollPrev(api.canScrollPrev());
- setCanScrollNext(api.canScrollNext());
- }, []);
- const scrollPrev = React.useCallback(() => {
- api?.scrollPrev();
- }, [api]);
- const scrollNext = React.useCallback(() => {
- api?.scrollNext();
- }, [api]);
- const handleKeyDown = React.useCallback(
- (event: React.KeyboardEvent<HTMLDivElement>) => {
- if (event.key === 'ArrowLeft') {
- event.preventDefault();
- scrollPrev();
- } else if (event.key === 'ArrowRight') {
- event.preventDefault();
- scrollNext();
- }
- },
- [scrollPrev, scrollNext],
- );
- React.useEffect(() => {
- if (!api || !setApi) return;
- setApi(api);
- }, [api, setApi]);
- React.useEffect(() => {
- if (!api) return;
- onSelect(api);
- api.on('reInit', onSelect);
- api.on('select', onSelect);
- return () => {
- api?.off('select', onSelect);
- };
- }, [api, onSelect]);
- return (
- <CarouselContext.Provider
- value={{
- carouselRef,
- api: api,
- opts,
- orientation: orientation || (opts?.axis === 'y' ? 'vertical' : 'horizontal'),
- scrollPrev,
- scrollNext,
- canScrollPrev,
- canScrollNext,
- }}
- >
- <div
- onKeyDownCapture={handleKeyDown}
- className={cn('relative', className)}
- role="region"
- aria-roledescription="carousel"
- data-slot="carousel"
- {...props}
- >
- {children}
- </div>
- </CarouselContext.Provider>
- );
- }
- function CarouselContent({ className, ...props }: React.ComponentProps<'div'>) {
- const { carouselRef, orientation } = useCarousel();
- return (
- <div ref={carouselRef} className="overflow-hidden" data-slot="carousel-content">
- <div
- className={cn('flex', orientation === 'horizontal' ? '-ml-4' : '-mt-4 flex-col', className)}
- {...props}
- />
- </div>
- );
- }
- function CarouselItem({ className, ...props }: React.ComponentProps<'div'>) {
- const { orientation } = useCarousel();
- return (
- <div
- role="group"
- aria-roledescription="slide"
- data-slot="carousel-item"
- className={cn(
- 'min-w-0 shrink-0 grow-0 basis-full',
- orientation === 'horizontal' ? 'pl-4' : 'pt-4',
- className,
- )}
- {...props}
- />
- );
- }
- function CarouselPrevious({
- className,
- variant = 'outline',
- size = 'icon-sm',
- ...props
- }: React.ComponentProps<typeof Button>) {
- const { orientation, scrollPrev, canScrollPrev } = useCarousel();
- return (
- <Button
- data-slot="carousel-previous"
- variant={variant}
- size={size}
- className={cn(
- 'rounded-full absolute touch-manipulation',
- orientation === 'horizontal'
- ? 'top-1/2 -left-12 -translate-y-1/2'
- : '-top-12 left-1/2 -translate-x-1/2 rotate-90',
- className,
- )}
- disabled={!canScrollPrev}
- onClick={scrollPrev}
- {...props}
- >
- <ChevronLeftIcon />
- <span className="sr-only">Previous slide</span>
- </Button>
- );
- }
- function CarouselNext({
- className,
- variant = 'outline',
- size = 'icon-sm',
- ...props
- }: React.ComponentProps<typeof Button>) {
- const { orientation, scrollNext, canScrollNext } = useCarousel();
- return (
- <Button
- data-slot="carousel-next"
- variant={variant}
- size={size}
- className={cn(
- 'rounded-full absolute touch-manipulation',
- orientation === 'horizontal'
- ? 'top-1/2 -right-12 -translate-y-1/2'
- : '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',
- className,
- )}
- disabled={!canScrollNext}
- onClick={scrollNext}
- {...props}
- >
- <ChevronRightIcon />
- <span className="sr-only">Next slide</span>
- </Button>
- );
- }
- export {
- type CarouselApi,
- Carousel,
- CarouselContent,
- CarouselItem,
- CarouselPrevious,
- CarouselNext,
- useCarousel,
- };
|