{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "carousel",
  "title": "Carousel",
  "description": "The Material carousel layouts — multi-browse, hero, uncontained, full-screen — sized by container query.",
  "dependencies": [
    "embla-carousel-react@^8.6.0",
    "lucide-react@^1.33.0"
  ],
  "registryDependencies": [
    "https://guillermo-rebolledo.github.io/materialcn/r/button.json",
    "https://guillermo-rebolledo.github.io/materialcn/r/materialcn-theme.json",
    "https://guillermo-rebolledo.github.io/materialcn/r/utils.json"
  ],
  "files": [
    {
      "path": "src/components/ui/carousel.tsx",
      "content": "import {\n  Children,\n  cloneElement,\n  isValidElement,\n  useCallback,\n  useEffect,\n  useRef,\n  useState,\n  type ComponentProps,\n  type KeyboardEvent,\n  type ReactElement,\n} from \"react\"\nimport useEmblaCarousel from \"embla-carousel-react\"\nimport { ChevronLeftIcon, ChevronRightIcon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"./button\"\nimport {\n  CarouselContext,\n  useCarousel,\n  type CarouselApi,\n} from \"./carousel-context\"\n\ntype UseCarouselParameters = Parameters<typeof useEmblaCarousel>\ntype CarouselOptions = UseCarouselParameters[0]\ntype CarouselPlugin = UseCarouselParameters[1]\ntype CarouselLayout =\n  | \"standard\"\n  | \"multi-browse\"\n  | \"hero\"\n  | \"uncontained\"\n  | \"full-screen\"\n\ntype CarouselAccessibleName =\n  | { \"aria-label\": string; \"aria-labelledby\"?: string }\n  | { \"aria-label\"?: never; \"aria-labelledby\": string }\n\ntype CarouselProps = Omit<\n  ComponentProps<\"div\">,\n  \"aria-label\" | \"aria-labelledby\" | \"role\"\n> &\n  CarouselAccessibleName & {\n    /** Material layout used to size and emphasize visible items. */\n    layout?: CarouselLayout\n    /** Embla options merged with the horizontal carousel defaults. */\n    opts?: CarouselOptions\n    /** Optional Embla plugins. */\n    plugins?: CarouselPlugin\n    /** Receives the Embla API after initialization. */\n    setApi?: (api: CarouselApi) => void\n  }\n\nconst carouselControlClassName =\n  \"absolute inset-y-0 my-auto touch-manipulation aria-disabled:pointer-events-none aria-disabled:cursor-not-allowed aria-disabled:bg-m3-on-surface/12 aria-disabled:text-m3-on-surface/38\"\n\nconst horizontalArrowConsumerSelector = [\n  \"input\",\n  \"textarea\",\n  \"select\",\n  '[contenteditable]:not([contenteditable=\"false\"])',\n  '[role=\"combobox\"]',\n  '[role=\"menuitemradio\"]',\n  '[role=\"option\"]',\n  '[role=\"radio\"]',\n  '[role=\"slider\"]',\n  '[role=\"spinbutton\"]',\n  '[role=\"tab\"]',\n  '[role=\"textbox\"]',\n].join(\",\")\n\nfunction consumesHorizontalArrowKey(target: EventTarget | null) {\n  return (\n    target instanceof Element &&\n    target.closest(horizontalArrowConsumerSelector) !== null\n  )\n}\n\nfunction usePrefersReducedMotion() {\n  const [prefersReducedMotion, setPrefersReducedMotion] = useState(\n    () =>\n      typeof window !== \"undefined\" &&\n      window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches,\n  )\n\n  useEffect(() => {\n    const query = window.matchMedia(\"(prefers-reduced-motion: reduce)\")\n    const updatePreference = () => setPrefersReducedMotion(query.matches)\n    updatePreference()\n    query.addEventListener(\"change\", updatePreference)\n    return () => query.removeEventListener(\"change\", updatePreference)\n  }, [])\n\n  return prefersReducedMotion\n}\n\n/** A touch-enabled Material carousel powered by Embla. */\nfunction Carousel({\n  children,\n  className,\n  layout = \"standard\",\n  onKeyDownCapture,\n  opts,\n  plugins,\n  setApi,\n  ...props\n}: CarouselProps) {\n  const prefersReducedMotion = usePrefersReducedMotion()\n  const [carouselRef, api] = useEmblaCarousel(\n    {\n      align: \"start\",\n      ...opts,\n      axis: \"x\",\n      ...(prefersReducedMotion ? { duration: 0 } : {}),\n    },\n    plugins,\n  )\n  const [selectedIndex, setSelectedIndex] = useState(opts?.startIndex ?? 0)\n  const intendedIndex = useRef(opts?.startIndex ?? 0)\n  const [canScrollPrevious, setCanScrollPrevious] = useState(false)\n  const [canScrollNext, setCanScrollNext] = useState(false)\n\n  const updateSelection = useCallback((nextApi: CarouselApi) => {\n    if (!nextApi) return\n    const nextIndex = nextApi.selectedScrollSnap()\n    intendedIndex.current = nextIndex\n    setSelectedIndex(nextIndex)\n    setCanScrollPrevious(nextApi.canScrollPrev())\n    setCanScrollNext(nextApi.canScrollNext())\n  }, [])\n\n  const restoreSelection = useCallback(\n    (nextApi: CarouselApi) => {\n      if (!nextApi) return\n      const lastSnap = nextApi.scrollSnapList().length - 1\n      const nextIndex = Math.max(0, Math.min(intendedIndex.current, lastSnap))\n      if (nextApi.selectedScrollSnap() !== nextIndex) {\n        nextApi.scrollTo(nextIndex, true)\n      }\n      updateSelection(nextApi)\n    },\n    [updateSelection],\n  )\n\n  const scrollPrevious = useCallback(() => api?.scrollPrev(), [api])\n  const scrollNext = useCallback(() => api?.scrollNext(), [api])\n\n  const handleKeyDownCapture = useCallback(\n    (event: KeyboardEvent<HTMLDivElement>) => {\n      onKeyDownCapture?.(event)\n      if (event.defaultPrevented) return\n      if (consumesHorizontalArrowKey(event.target)) return\n\n      if (event.key === \"ArrowLeft\") {\n        event.preventDefault()\n        scrollPrevious()\n      } else if (event.key === \"ArrowRight\") {\n        event.preventDefault()\n        scrollNext()\n      }\n    },\n    [onKeyDownCapture, scrollNext, scrollPrevious],\n  )\n\n  useEffect(() => {\n    if (api && setApi) setApi(api)\n  }, [api, setApi])\n\n  useEffect(() => {\n    if (!api) return\n\n    const initialSelectionFrame = requestAnimationFrame(() =>\n      updateSelection(api),\n    )\n    api.on(\"select\", updateSelection)\n    api.on(\"reInit\", restoreSelection)\n\n    return () => {\n      cancelAnimationFrame(initialSelectionFrame)\n      api.off(\"select\", updateSelection)\n      api.off(\"reInit\", restoreSelection)\n    }\n  }, [api, restoreSelection, updateSelection])\n\n  return (\n    <CarouselContext.Provider\n      value={{\n        api,\n        canScrollNext,\n        canScrollPrevious,\n        carouselRef,\n        scrollNext,\n        scrollPrevious,\n        selectedIndex,\n      }}\n    >\n      <div\n        {...props}\n        role=\"region\"\n        aria-roledescription=\"carousel\"\n        data-align={opts?.align === \"center\" ? \"center\" : \"start\"}\n        data-layout={layout}\n        data-reduced-motion={prefersReducedMotion || undefined}\n        data-slot=\"carousel\"\n        onKeyDownCapture={handleKeyDownCapture}\n        className={cn(\"m3-carousel relative w-full\", className)}\n      >\n        {children}\n      </div>\n    </CarouselContext.Provider>\n  )\n}\n\nfunction CarouselContent({\n  children,\n  className,\n  ...props\n}: ComponentProps<\"div\">) {\n  const { carouselRef, selectedIndex } = useCarousel()\n  const slides = Children.toArray(children).filter(isValidElement)\n  const slideCount = slides.length\n\n  return (\n    <div\n      ref={carouselRef}\n      className=\"overflow-hidden\"\n      data-slot=\"carousel-viewport\"\n    >\n      <div\n        {...props}\n        data-slot=\"carousel-content\"\n        className={cn(\"flex gap-2 px-4 py-2\", className)}\n      >\n        {slides.map((child, index) => {\n          const slide = child as ReactElement<ComponentProps<\"div\">>\n          const accessibleName =\n            slide.props[\"aria-label\"] || slide.props[\"aria-labelledby\"]\n              ? undefined\n              : `Slide ${index + 1} of ${slideCount}`\n\n          return cloneElement(\n            slide,\n            {\n              \"aria-label\": accessibleName ?? slide.props[\"aria-label\"],\n              \"aria-current\": selectedIndex === index ? \"true\" : undefined,\n              \"data-selected\": selectedIndex === index ? \"true\" : undefined,\n            } as ComponentProps<\"div\">,\n          )\n        })}\n      </div>\n    </div>\n  )\n}\n\nfunction CarouselItem({ className, ...props }: ComponentProps<\"div\">) {\n  return (\n    <div\n      {...props}\n      role=\"group\"\n      aria-roledescription=\"slide\"\n      data-slot=\"carousel-item\"\n      className={cn(\n        \"min-w-0 shrink-0 grow-0 basis-full overflow-hidden rounded-[28px]\",\n        className,\n      )}\n    />\n  )\n}\n\ntype CarouselControlDirection = \"previous\" | \"next\"\n\nfunction CarouselControl({\n  direction,\n  \"aria-label\": ariaLabel,\n  className,\n  disabled,\n  onClick,\n  shape = \"round\",\n  size = \"icon\",\n  variant = \"tonal\",\n  ...props\n}: ComponentProps<typeof Button> & { direction: CarouselControlDirection }) {\n  const carousel = useCarousel()\n  const isPrevious = direction === \"previous\"\n  const canScroll = isPrevious\n    ? carousel.canScrollPrevious\n    : carousel.canScrollNext\n  const scroll = isPrevious ? carousel.scrollPrevious : carousel.scrollNext\n  const Icon = isPrevious ? ChevronLeftIcon : ChevronRightIcon\n  const isUnavailable = Boolean(disabled || !canScroll)\n\n  return (\n    <Button\n      {...props}\n      type=\"button\"\n      aria-label={\n        ariaLabel ?? (isPrevious ? \"Previous slide\" : \"Next slide\")\n      }\n      data-slot={`carousel-${direction}`}\n      variant={variant}\n      shape={shape}\n      size={size}\n      className={cn(\n        carouselControlClassName,\n        isPrevious ? \"left-6\" : \"right-6\",\n        className,\n      )}\n      disabled={disabled}\n      aria-disabled={isUnavailable || undefined}\n      onClick={(event) => {\n        if (isUnavailable) {\n          event.preventDefault()\n          return\n        }\n        onClick?.(event)\n        if (!event.defaultPrevented) scroll()\n      }}\n    >\n      <Icon aria-hidden=\"true\" />\n    </Button>\n  )\n}\n\nfunction CarouselPrevious(props: ComponentProps<typeof Button>) {\n  return <CarouselControl {...props} direction=\"previous\" />\n}\n\nfunction CarouselNext(props: ComponentProps<typeof Button>) {\n  return <CarouselControl {...props} direction=\"next\" />\n}\n\nexport {\n  Carousel,\n  CarouselContent,\n  CarouselItem,\n  CarouselNext,\n  CarouselPrevious,\n  useCarousel,\n}\nexport type {\n  CarouselAccessibleName,\n  CarouselApi,\n  CarouselLayout,\n  CarouselOptions,\n  CarouselPlugin,\n  CarouselProps,\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "src/components/ui/carousel-context.ts",
      "content": "import { createContext, useContext } from \"react\"\nimport type { UseEmblaCarouselType } from \"embla-carousel-react\"\n\ntype CarouselApi = UseEmblaCarouselType[1]\n\ntype CarouselContextValue = {\n  api: CarouselApi\n  canScrollNext: boolean\n  canScrollPrevious: boolean\n  carouselRef: UseEmblaCarouselType[0]\n  scrollNext: () => void\n  scrollPrevious: () => void\n  selectedIndex: number\n}\n\nconst CarouselContext = createContext<CarouselContextValue | null>(null)\n\nfunction useCarousel() {\n  const context = useContext(CarouselContext)\n\n  if (!context) {\n    throw new Error(\"useCarousel must be used within a <Carousel />\")\n  }\n\n  return context\n}\n\nexport { CarouselContext, useCarousel }\nexport type { CarouselApi, CarouselContextValue }\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "display"
  ],
  "type": "registry:ui"
}