{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "carousel",
  "type": "registry:ui",
  "registryDependencies": [],
  "dependencies": ["embla-carousel-react", "lucide-react"],
  "devDependencies": [],
  "files": [
    {
      "path": "carousel.tsx",
      "content": "\"use client\";\n\nimport useEmblaCarousel, {\n  type UseEmblaCarouselType,\n} from \"embla-carousel-react\";\nimport { ChevronLeftIcon, ChevronRightIcon } from \"lucide-react\";\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst componentThemeClassName =\n  \"[--ic-background:#ffffff] [--ic-foreground:#111111] [--ic-primary:#111111] [--ic-secondary:#646b75] [--ic-surface-border:#e9edf2] [--ic-border:#e3e7ec] [--ic-card:#ffffff] [--ic-card-foreground:#111111] [--ic-muted:#f5f7fa] [--ic-muted-foreground:#6d7480] [--ic-accent:#f3f5f8] [--color-accent:var(--ic-accent)] [--color-accent-foreground:var(--ic-accent-foreground)] [--ic-accent-foreground:#111111] [--ic-input:#e3e7ec] [--ic-ring:rgba(17,17,17,0.16)] [--ic-destructive:#dc2626] [--ic-paper:#fcfcfd] [--ic-popover-foreground:#111111] [--ic-brand:#0ea5e9] [--ic-brand-soft:#bae6fd] [--color-background:var(--ic-background)] [--color-foreground:var(--ic-foreground)] [--color-primary:var(--ic-primary)] [--color-secondary:var(--ic-secondary)] [--color-border:var(--ic-border)] [--color-card:var(--ic-card)] [--color-card-foreground:var(--ic-card-foreground)] [--color-muted:var(--ic-muted)] [--color-muted-foreground:var(--ic-muted-foreground)] [--color-accent-foreground:var(--ic-accent-foreground)] [--color-input:var(--ic-input)] [--color-ring:var(--ic-ring)] [--color-destructive:var(--ic-destructive)] [--color-paper:var(--ic-paper)] [--color-popover-foreground:var(--ic-popover-foreground)] [--color-brand:var(--ic-brand)] [--color-brand-soft:var(--ic-brand-soft)] dark:[--ic-background:#111111] dark:[--ic-foreground:#f6f3ec] dark:[--ic-primary:#f6f3ec] dark:[--ic-secondary:#cbc6bb] dark:[--ic-surface-border:#2a2a25] dark:[--ic-border:#2b2a25] dark:[--ic-card:#111111] dark:[--ic-card-foreground:#f6f3ec] dark:[--ic-muted:#171716] dark:[--ic-muted-foreground:#9a958a] dark:[--ic-accent:#1a1a18] dark:[--ic-accent-foreground:#f6f3ec] dark:[--ic-input:#2b2a25] dark:[--ic-ring:rgba(246,243,236,0.18)] dark:[--ic-destructive:#f87171] dark:[--ic-paper:#171716] dark:[--ic-popover-foreground:#f6f3ec] dark:[--ic-brand:#38bdf8] dark:[--ic-brand-soft:#0c4a6e] dark:[--ic-chart-1:oklch(0.68_0.17_250)]\";\n\ntype CarouselApi = UseEmblaCarouselType[1];\ntype UseCarouselParameters = Parameters<typeof useEmblaCarousel>;\ntype CarouselOptions = UseCarouselParameters[0];\ntype CarouselPlugin = UseCarouselParameters[1];\n\nconst carouselAspectRatioClasses = {\n  square: \"aspect-square\",\n  video: \"aspect-video\",\n  \"4/3\": \"aspect-[4/3]\",\n  \"3/2\": \"aspect-[3/2]\",\n  portrait: \"aspect-[3/4]\",\n} as const;\n\nconst CUSTOM_ASPECT_RATIO_PATTERN = /^\\d+(\\.\\d+)?\\/\\d+(\\.\\d+)?$/;\n\nconst AUTOPLAY_DEFAULT_DELAY_MS = 4000;\n\ntype CarouselAspectRatio = keyof typeof carouselAspectRatioClasses;\n\ntype CarouselNavPlacement = \"outside\" | \"responsive\";\n\ntype CarouselProps = {\n  opts?: CarouselOptions;\n  plugins?: CarouselPlugin;\n  orientation?: \"horizontal\" | \"vertical\";\n  aspectRatio?: CarouselAspectRatio | `${number}/${number}`;\n  navPlacement?: CarouselNavPlacement;\n  autoplay?: boolean | number;\n  setApi?: (api: CarouselApi) => void;\n};\n\nfunction getCarouselAspectRatioClassName(\n  aspectRatio?: CarouselProps[\"aspectRatio\"]\n) {\n  if (!aspectRatio) {\n    return undefined;\n  }\n\n  if (aspectRatio in carouselAspectRatioClasses) {\n    return carouselAspectRatioClasses[aspectRatio as CarouselAspectRatio];\n  }\n\n  return undefined;\n}\n\nfunction getCarouselAspectRatioStyle(\n  aspectRatio?: CarouselProps[\"aspectRatio\"]\n): React.CSSProperties | undefined {\n  if (!aspectRatio || aspectRatio in carouselAspectRatioClasses) {\n    return undefined;\n  }\n\n  if (CUSTOM_ASPECT_RATIO_PATTERN.test(aspectRatio)) {\n    const [width, height] = aspectRatio.split(\"/\");\n\n    return {\n      aspectRatio: `${width} / ${height}`,\n    };\n  }\n\n  return undefined;\n}\n\nfunction getAutoplayDelay(autoplay: boolean | number) {\n  return typeof autoplay === \"number\" ? autoplay : AUTOPLAY_DEFAULT_DELAY_MS;\n}\n\nfunction isEditableTarget(target: EventTarget | null) {\n  if (!(target instanceof HTMLElement)) {\n    return false;\n  }\n\n  return Boolean(\n    target.isContentEditable ||\n      target.closest(\n        \"input, textarea, select, [contenteditable=''], [contenteditable='true']\"\n      )\n  );\n}\n\ntype CarouselContextProps = {\n  carouselRef: ReturnType<typeof useEmblaCarousel>[0];\n  api: ReturnType<typeof useEmblaCarousel>[1];\n  scrollPrev: () => void;\n  scrollNext: () => void;\n  scrollTo: (index: number) => void;\n  canScrollPrev: boolean;\n  canScrollNext: boolean;\n  selectedIndex: number;\n  scrollSnapCount: number;\n  navPlacement: CarouselNavPlacement;\n} & CarouselProps;\n\nconst CarouselContext = React.createContext<CarouselContextProps | null>(null);\n\nconst CarouselSlideIndexContext = React.createContext<number | undefined>(\n  undefined\n);\n\nfunction useCarousel() {\n  const context = React.useContext(CarouselContext);\n\n  if (!context) {\n    throw new Error(\"useCarousel must be used within a <Carousel />\");\n  }\n\n  return context;\n}\n\nconst carouselNavPositionClasses = {\n  horizontal: {\n    responsive: {\n      previous:\n        \"top-full right-10 mt-1.5 sm:top-1/2 sm:right-auto sm:mt-0 sm:-left-12 sm:-translate-y-1/2\",\n      next: \"top-full right-0 mt-1.5 sm:top-1/2 sm:-right-12 sm:mt-0 sm:-translate-y-1/2\",\n    },\n    outside: {\n      previous: \"top-1/2 -left-12 -translate-y-1/2\",\n      next: \"top-1/2 -right-12 -translate-y-1/2\",\n    },\n  },\n  vertical: {\n    responsive: {\n      previous:\n        \"top-2 left-1/2 -translate-x-1/2 rotate-90 sm:-top-12 sm:translate-y-0\",\n      next: \"bottom-2 left-1/2 -translate-x-1/2 rotate-90 sm:-bottom-12 sm:translate-y-0\",\n    },\n    outside: {\n      previous: \"-top-12 left-1/2 -translate-x-1/2 rotate-90\",\n      next: \"-bottom-12 left-1/2 -translate-x-1/2 rotate-90\",\n    },\n  },\n} as const;\n\nfunction getNavPlacementClasses(\n  placement: CarouselNavPlacement,\n  orientation: \"horizontal\" | \"vertical\",\n  direction: \"previous\" | \"next\"\n) {\n  return carouselNavPositionClasses[orientation][placement][direction];\n}\n\nfunction Carousel({\n  orientation = \"horizontal\",\n  opts,\n  setApi,\n  plugins,\n  aspectRatio = \"video\",\n  navPlacement = \"responsive\",\n  autoplay,\n  className,\n  children,\n  \"aria-label\": ariaLabel = \"Carousel\",\n  ...props\n}: React.ComponentProps<\"section\"> & CarouselProps) {\n  const resolvedOrientation =\n    orientation || (opts?.axis === \"y\" ? \"vertical\" : \"horizontal\");\n  const emblaOptions = React.useMemo(\n    () => ({\n      ...opts,\n      loop: Boolean(opts?.loop),\n      axis:\n        resolvedOrientation === \"horizontal\" ? (\"x\" as const) : (\"y\" as const),\n    }),\n    [opts, resolvedOrientation]\n  );\n  const optsRef = React.useRef(emblaOptions);\n  optsRef.current = emblaOptions;\n  const autoplayPausedUntilRef = React.useRef(0);\n  const isAutoAdvancingRef = React.useRef(false);\n\n  const [carouselRef, api] = useEmblaCarousel(emblaOptions, plugins);\n  const [canScrollPrev, setCanScrollPrev] = React.useState(false);\n  const [canScrollNext, setCanScrollNext] = React.useState(false);\n  const [selectedIndex, setSelectedIndex] = React.useState(0);\n  const [scrollSnapCount, setScrollSnapCount] = React.useState(0);\n\n  const onSelect = React.useCallback((carouselApi: CarouselApi) => {\n    if (!carouselApi) return;\n\n    setCanScrollPrev(carouselApi.canScrollPrev());\n    setCanScrollNext(carouselApi.canScrollNext());\n    setSelectedIndex(carouselApi.selectedScrollSnap());\n    setScrollSnapCount(carouselApi.scrollSnapList().length);\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 scrollTo = React.useCallback(\n    (index: number) => {\n      api?.scrollTo(index);\n    },\n    [api]\n  );\n\n  const pauseAutoplay = React.useCallback(() => {\n    if (!autoplay) return;\n\n    autoplayPausedUntilRef.current = Date.now() + getAutoplayDelay(autoplay);\n  }, [autoplay]);\n\n  const handleKeyDown = React.useCallback(\n    (event: React.KeyboardEvent<HTMLElement>) => {\n      if (isEditableTarget(event.target)) {\n        return;\n      }\n\n      const isVertical = resolvedOrientation === \"vertical\";\n\n      if (!isVertical && event.key === \"ArrowLeft\") {\n        event.preventDefault();\n        scrollPrev();\n        return;\n      }\n\n      if (!isVertical && event.key === \"ArrowRight\") {\n        event.preventDefault();\n        scrollNext();\n        return;\n      }\n\n      if (isVertical && event.key === \"ArrowUp\") {\n        event.preventDefault();\n        scrollPrev();\n        return;\n      }\n\n      if (isVertical && event.key === \"ArrowDown\") {\n        event.preventDefault();\n        scrollNext();\n      }\n    },\n    [resolvedOrientation, scrollNext, scrollPrev]\n  );\n\n  React.useEffect(() => {\n    if (!setApi) return;\n\n    if (api) {\n      setApi(api);\n    }\n  }, [api, setApi]);\n\n  React.useEffect(() => {\n    if (!api) return;\n\n    onSelect(api);\n    api.on(\"reInit\", onSelect);\n    api.on(\"select\", onSelect);\n\n    return () => {\n      api.off(\"reInit\", onSelect);\n      api.off(\"select\", onSelect);\n    };\n  }, [api, onSelect]);\n\n  React.useEffect(() => {\n    if (!(api && autoplay)) return;\n\n    const delay = getAutoplayDelay(autoplay);\n\n    const onPointerDown = () => {\n      pauseAutoplay();\n    };\n\n    const onManualSelect = () => {\n      if (isAutoAdvancingRef.current) {\n        isAutoAdvancingRef.current = false;\n        return;\n      }\n\n      pauseAutoplay();\n    };\n\n    api.on(\"pointerDown\", onPointerDown);\n    api.on(\"select\", onManualSelect);\n\n    const intervalId = window.setInterval(() => {\n      if (Date.now() < autoplayPausedUntilRef.current) {\n        return;\n      }\n\n      if (api.canScrollNext()) {\n        isAutoAdvancingRef.current = true;\n        api.scrollNext();\n        return;\n      }\n\n      if (optsRef.current.loop) {\n        isAutoAdvancingRef.current = true;\n        api.scrollTo(0);\n      }\n    }, delay);\n\n    return () => {\n      api.off(\"pointerDown\", onPointerDown);\n      api.off(\"select\", onManualSelect);\n      window.clearInterval(intervalId);\n    };\n  }, [api, autoplay, pauseAutoplay]);\n\n  return (\n    <CarouselContext.Provider\n      value={{\n        carouselRef,\n        api,\n        opts,\n        orientation: resolvedOrientation,\n        aspectRatio,\n        navPlacement,\n        autoplay,\n        scrollPrev,\n        scrollNext,\n        scrollTo,\n        canScrollPrev,\n        canScrollNext,\n        selectedIndex,\n        scrollSnapCount,\n        setApi,\n      }}\n    >\n      <section\n        aria-label={ariaLabel}\n        aria-roledescription=\"carousel\"\n        className={cn(\n          componentThemeClassName,\n          \"relative outline-none\",\n          className\n        )}\n        data-slot=\"carousel\"\n        onKeyDown={handleKeyDown}\n        // biome-ignore lint/a11y/noNoninteractiveTabindex: carousel keyboard navigation requires a focusable region\n        tabIndex={0}\n        {...props}\n      >\n        <div aria-atomic=\"true\" aria-live=\"polite\" className=\"sr-only\">\n          {scrollSnapCount > 0\n            ? `Slide ${selectedIndex + 1} of ${scrollSnapCount}`\n            : null}\n        </div>\n        {children}\n      </section>\n    </CarouselContext.Provider>\n  );\n}\n\nfunction CarouselContent({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  const { carouselRef, orientation, aspectRatio } = useCarousel();\n  const aspectClassName = getCarouselAspectRatioClassName(aspectRatio);\n  const aspectStyle = getCarouselAspectRatioStyle(aspectRatio);\n  const panClassName =\n    orientation === \"horizontal\" ? \"touch-pan-y\" : \"touch-pan-x\";\n\n  return (\n    <div\n      className={cn(\n        \"overflow-hidden rounded-lg border border-border\",\n        panClassName,\n        aspectClassName\n      )}\n      data-slot=\"carousel-content\"\n      ref={carouselRef}\n      style={aspectStyle}\n    >\n      <div\n        className={cn(\n          \"flex select-none\",\n          aspectClassName || aspectStyle ? \"h-full\" : undefined,\n          orientation === \"horizontal\" ? \"-ml-4\" : \"-mt-4 flex-col\",\n          className\n        )}\n        {...props}\n      >\n        {React.Children.map(children, (child, index) => {\n          if (!React.isValidElement(child)) {\n            return child;\n          }\n\n          return (\n            <CarouselSlideIndexContext.Provider\n              key={child.key ?? index}\n              value={index}\n            >\n              {child}\n            </CarouselSlideIndexContext.Provider>\n          );\n        })}\n      </div>\n    </div>\n  );\n}\n\nfunction CarouselItem({ className, ...props }: React.ComponentProps<\"div\">) {\n  const slideIndex = React.useContext(CarouselSlideIndexContext);\n  const { orientation, aspectRatio, selectedIndex } = useCarousel();\n  const hasAspectRatio = Boolean(\n    getCarouselAspectRatioClassName(aspectRatio) ||\n      getCarouselAspectRatioStyle(aspectRatio)\n  );\n  const isActive = slideIndex !== undefined && slideIndex === selectedIndex;\n\n  return (\n    // biome-ignore lint/a11y/useSemanticElements: fieldset implies form grouping, not slide content\n    <div\n      aria-hidden={slideIndex === undefined ? undefined : !isActive}\n      aria-label={\n        slideIndex === undefined ? undefined : `Slide ${slideIndex + 1}`\n      }\n      aria-roledescription=\"slide\"\n      className={cn(\n        \"min-w-0 shrink-0 grow-0 basis-full\",\n        hasAspectRatio && \"h-full\",\n        orientation === \"horizontal\" ? \"pl-4\" : \"pt-4\",\n        className\n      )}\n      data-slot=\"carousel-item\"\n      role=\"group\"\n      {...props}\n    />\n  );\n}\n\nconst carouselNavButtonClassName =\n  \"absolute z-10 inline-flex size-8 touch-manipulation items-center justify-center rounded-full bg-muted/70 text-muted-foreground transition-colors duration-200 hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-35 sm:size-9 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 sm:[&_svg]:size-[1.125rem]\";\n\nfunction CarouselNavButton({\n  className,\n  ...props\n}: React.ComponentPropsWithoutRef<\"button\">) {\n  return (\n    <button\n      className={cn(carouselNavButtonClassName, className)}\n      type=\"button\"\n      {...props}\n    />\n  );\n}\n\nfunction CarouselPrevious({\n  className,\n  onClick,\n  ...props\n}: React.ComponentPropsWithoutRef<\"button\">) {\n  const {\n    orientation = \"horizontal\",\n    navPlacement = \"responsive\",\n    scrollPrev,\n    canScrollPrev,\n  } = useCarousel();\n  const placementClassName = getNavPlacementClasses(\n    navPlacement,\n    orientation,\n    \"previous\"\n  );\n\n  return (\n    <CarouselNavButton\n      className={cn(placementClassName, className)}\n      data-slot=\"carousel-previous\"\n      disabled={!canScrollPrev}\n      onClick={(event) => {\n        scrollPrev();\n        onClick?.(event);\n      }}\n      {...props}\n    >\n      <ChevronLeftIcon className=\"cn-rtl-flip\" strokeWidth={2.25} />\n      <span className=\"sr-only\">Previous slide</span>\n    </CarouselNavButton>\n  );\n}\n\nfunction CarouselNext({\n  className,\n  onClick,\n  ...props\n}: React.ComponentPropsWithoutRef<\"button\">) {\n  const {\n    orientation = \"horizontal\",\n    navPlacement = \"responsive\",\n    scrollNext,\n    canScrollNext,\n  } = useCarousel();\n  const placementClassName = getNavPlacementClasses(\n    navPlacement,\n    orientation,\n    \"next\"\n  );\n\n  return (\n    <CarouselNavButton\n      className={cn(placementClassName, className)}\n      data-slot=\"carousel-next\"\n      disabled={!canScrollNext}\n      onClick={(event) => {\n        scrollNext();\n        onClick?.(event);\n      }}\n      {...props}\n    >\n      <ChevronRightIcon className=\"cn-rtl-flip\" strokeWidth={2.25} />\n      <span className=\"sr-only\">Next slide</span>\n    </CarouselNavButton>\n  );\n}\n\nexport {\n  type CarouselApi,\n  type CarouselAspectRatio,\n  type CarouselNavPlacement,\n  type CarouselOptions,\n  type CarouselPlugin,\n  type CarouselProps,\n  Carousel,\n  CarouselContent,\n  CarouselItem,\n  CarouselPrevious,\n  CarouselNext,\n  useCarousel,\n};\n",
      "type": "registry:ui"
    }
  ],
  "title": "Carousel",
  "description": "Embla-powered carousel with aspect-ratio presets, responsive navigation placement, and horizontal or vertical slides. Built with embla-carousel-react and Lucide."
}
