{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "carousel",
  "title": "Carousel",
  "author": "@fma-ds/ds",
  "description": "A carousel for cycling through slides (images, cards, or any content) with keyboard, swipe, and button navigation.\n\nBuilt on embla-carousel-react; `CarouselPrevious`/`CarouselNext` are `Button`s wired to the carousel API.\n\n- **Autoplay**: `autoplay` (`true` or `{ delay }`, 5000 ms by default) advances on its own and wraps to the first slide. Rotation pauses while the pointer is over the carousel or focus is inside it, and starts paused under reduced motion.\n- **`CarouselAutoplayToggle`**: pause/resume — required for moving content by WCAG 2.2.2; a dev warning flags its absence.\n- **`CarouselDots`**: one dot per slide; marks the current one and goes to a slide on press.\n- **Announcements**: slide changes are announced (`aria-live`) only when not rotating.\n\nThe toggle and dots are not positioned; place them where they fit.\n\nBuilt-in labels are in English; translate them with `i18n.labels`.",
  "dependencies": [
    "cn@^0.5.0",
    "embla-carousel-react@^8.6.0",
    "lucide-react@^1.61.0"
  ],
  "registryDependencies": [
    "@fma-ds/button"
  ],
  "files": [
    {
      "path": "src/core/carousel/carousel.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"cn\";\nimport useEmblaCarousel, { type UseEmblaCarouselType } from \"embla-carousel-react\";\nimport { ChevronLeftIcon, ChevronRightIcon, PauseIcon, PlayIcon } from \"lucide-react\";\nimport * as React from \"react\";\nimport { Button } from \"@/core/button/button\";\n\ntype CarouselApi = UseEmblaCarouselType[1];\ntype UseCarouselParameters = Parameters<typeof useEmblaCarousel>;\ntype CarouselOptions = UseCarouselParameters[0];\ntype CarouselPlugin = UseCarouselParameters[1];\n\ntype CarouselLabels = {\n  previous: string;\n  next: string;\n  pause: string;\n  /** Starts the rotation (also when reduced motion started it paused). */\n  start: string;\n  /** A dot's label, given the slide number (1-based) and the total. */\n  goToSlide: (slide: number, total: number) => string;\n};\n\ntype CarouselI18n = { labels?: Partial<CarouselLabels> };\n\nconst defaultCarouselLabels: CarouselLabels = {\n  previous: \"Previous slide\",\n  next: \"Next slide\",\n  pause: \"Pause slides\",\n  start: \"Play slides\",\n  goToSlide: (slide, total) => `Go to slide ${slide} of ${total}`,\n};\n\ntype CarouselProps = {\n  opts?: CarouselOptions;\n  plugins?: CarouselPlugin;\n  orientation?: \"horizontal\" | \"vertical\";\n  setApi?: (api: CarouselApi) => void;\n  /**\n   * Advances on its own every `delay` ms (5000 by default), back to the first\n   * slide after the last. Pair it with `CarouselAutoplayToggle`.\n   */\n  autoplay?: boolean | { delay?: number };\n  i18n?: CarouselI18n;\n};\n\n// Bundlers replace process.env.NODE_ENV at build time; declared here so\n// the component doesn't need Node types.\ndeclare const process: { env: { NODE_ENV?: string } };\n\nconst DEFAULT_AUTOPLAY_DELAY = 5000;\n\ntype CarouselContextProps = {\n  carouselRef: ReturnType<typeof useEmblaCarousel>[0];\n  api: ReturnType<typeof useEmblaCarousel>[1];\n  scrollPrev: () => void;\n  scrollNext: () => void;\n  canScrollPrev: boolean;\n  canScrollNext: boolean;\n  labels: CarouselLabels;\n  selectedIndex: number;\n  slideCount: number;\n  scrollTo: (index: number) => void;\n  hasAutoplay: boolean;\n  isPlaying: boolean;\n  setPlaying: (isPlaying: boolean) => void;\n  registerAutoplayToggle: () => () => void;\n} & Omit<CarouselProps, \"autoplay\" | \"i18n\">;\n\nconst CarouselContext = React.createContext<CarouselContextProps | null>(null);\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\nfunction Carousel({\n  orientation = \"horizontal\",\n  opts,\n  setApi,\n  plugins,\n  autoplay,\n  i18n,\n  className,\n  children,\n  onPointerMove,\n  onPointerLeave,\n  onFocus,\n  onBlur,\n  ...props\n}: React.ComponentProps<\"div\"> & CarouselProps) {\n  const labels = { ...defaultCarouselLabels, ...i18n?.labels };\n  const [carouselRef, api] = useEmblaCarousel(\n    {\n      ...opts,\n      axis: orientation === \"horizontal\" ? \"x\" : \"y\",\n    },\n    plugins,\n  );\n  const [canScrollPrev, setCanScrollPrev] = React.useState(false);\n  const [canScrollNext, setCanScrollNext] = React.useState(false);\n\n  const [selectedIndex, setSelectedIndex] = React.useState(0);\n  const [slideCount, setSlideCount] = React.useState(0);\n\n  const onSelect = React.useCallback((api: CarouselApi) => {\n    if (!api) return;\n    setCanScrollPrev(api.canScrollPrev());\n    setCanScrollNext(api.canScrollNext());\n    setSelectedIndex(api.selectedScrollSnap());\n    setSlideCount(api.scrollSnapList().length);\n  }, []);\n\n  const scrollTo = React.useCallback((index: number) => api?.scrollTo(index), [api]);\n\n  // Autoplay: the user's choice (the toggle), suspended while the pointer is\n  // over the carousel or focus is inside it (except on the toggle itself).\n  const hasAutoplay = Boolean(autoplay);\n  const delay = (typeof autoplay === \"object\" && autoplay.delay) || DEFAULT_AUTOPLAY_DELAY;\n  const [isPlaying, setPlaying] = React.useState(hasAutoplay);\n  const [isHovered, setHovered] = React.useState(false);\n  const [hasFocusWithin, setFocusWithin] = React.useState(false);\n  const isRotating = hasAutoplay && isPlaying && !isHovered && !hasFocusWithin;\n\n  // Reduced motion: start paused; the toggle can still start it.\n  React.useEffect(() => {\n    if (hasAutoplay && window.matchMedia?.(\"(prefers-reduced-motion: reduce)\").matches) {\n      setPlaying(false);\n    }\n  }, [hasAutoplay]);\n\n  // Re-armed on every slide change, so manual navigation restarts the delay.\n  React.useEffect(() => {\n    if (!api || !isRotating || selectedIndex < 0) return;\n    const timer = setTimeout(() => {\n      if (api.canScrollNext()) api.scrollNext();\n      else api.scrollTo(0);\n    }, delay);\n    return () => clearTimeout(timer);\n  }, [api, isRotating, delay, selectedIndex]);\n\n  // A ref, not state: the toggle's effect runs before this component's, so\n  // the count is already up to date when the check below runs.\n  const toggleCount = React.useRef(0);\n  const registerAutoplayToggle = React.useCallback(() => {\n    toggleCount.current += 1;\n    return () => {\n      toggleCount.current -= 1;\n    };\n  }, []);\n\n  React.useEffect(() => {\n    if (process.env.NODE_ENV !== \"production\" && hasAutoplay && toggleCount.current === 0) {\n      console.warn(\n        \"[carousel] `autoplay` without a `CarouselAutoplayToggle`: users need a way to pause moving content (WCAG 2.2.2).\",\n      );\n    }\n  }, [hasAutoplay]);\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<HTMLDivElement>) => {\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) return;\n    setApi(api);\n  }, [api, setApi]);\n\n  React.useEffect(() => {\n    if (!api) return;\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  return (\n    <CarouselContext.Provider\n      value={{\n        carouselRef,\n        api: api,\n        opts,\n        orientation: orientation || (opts?.axis === \"y\" ? \"vertical\" : \"horizontal\"),\n        scrollPrev,\n        scrollNext,\n        canScrollPrev,\n        canScrollNext,\n        labels,\n        selectedIndex,\n        slideCount,\n        scrollTo,\n        hasAutoplay,\n        isPlaying,\n        setPlaying,\n        registerAutoplayToggle,\n      }}\n    >\n      <div\n        onKeyDownCapture={handleKeyDown}\n        className={cn(\"relative\", className)}\n        role=\"region\"\n        aria-roledescription=\"carousel\"\n        data-slot=\"carousel\"\n        data-rotating={isRotating || undefined}\n        // Hover is a moving mouse: slides passing under a still cursor don't pause it.\n        onPointerMove={(event) => {\n          if (event.pointerType === \"mouse\") setHovered(true);\n          onPointerMove?.(event);\n        }}\n        onPointerLeave={(event) => {\n          setHovered(false);\n          onPointerLeave?.(event);\n        }}\n        onFocus={(event) => {\n          setFocusWithin(\n            !(event.target as Element).closest(\"[data-slot=carousel-autoplay-toggle]\"),\n          );\n          onFocus?.(event);\n        }}\n        onBlur={(event) => {\n          if (!event.currentTarget.contains(event.relatedTarget)) setFocusWithin(false);\n          onBlur?.(event);\n        }}\n        {...props}\n      >\n        {children}\n      </div>\n    </CarouselContext.Provider>\n  );\n}\n\nfunction CarouselContent({ className, ...props }: React.ComponentProps<\"div\">) {\n  const { carouselRef, orientation, hasAutoplay, isPlaying } = useCarousel();\n\n  return (\n    <div ref={carouselRef} className=\"overflow-hidden\" data-slot=\"carousel-content\">\n      <div\n        // Slide changes are announced only when the user drives them.\n        aria-live={hasAutoplay && isPlaying ? \"off\" : \"polite\"}\n        className={cn(\"flex\", orientation === \"horizontal\" ? \"-ml-4\" : \"-mt-4 flex-col\", className)}\n        {...props}\n      />\n    </div>\n  );\n}\n\nfunction CarouselItem({ className, ...props }: React.ComponentProps<\"div\">) {\n  const { orientation } = useCarousel();\n\n  return (\n    <div\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\",\n        orientation === \"horizontal\" ? \"pl-4\" : \"pt-4\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction CarouselPrevious({\n  className,\n  variant = \"outline\",\n  size = \"icon-sm\",\n  ...props\n}: React.ComponentProps<typeof Button>) {\n  const { orientation, scrollPrev, canScrollPrev, labels } = useCarousel();\n\n  return (\n    <Button\n      data-slot=\"carousel-previous\"\n      variant={variant}\n      size={size}\n      className={cn(\n        \"absolute touch-manipulation rounded-full\",\n        orientation === \"horizontal\"\n          ? \"inset-y-0 -left-12 my-auto\"\n          : \"-top-12 left-1/2 -translate-x-1/2 rotate-90\",\n        className,\n      )}\n      isDisabled={!canScrollPrev}\n      onPress={scrollPrev}\n      {...props}\n    >\n      <ChevronLeftIcon />\n      <span className=\"sr-only\">{labels.previous}</span>\n    </Button>\n  );\n}\n\nfunction CarouselNext({\n  className,\n  variant = \"outline\",\n  size = \"icon-sm\",\n  ...props\n}: React.ComponentProps<typeof Button>) {\n  const { orientation, scrollNext, canScrollNext, labels } = useCarousel();\n\n  return (\n    <Button\n      data-slot=\"carousel-next\"\n      variant={variant}\n      size={size}\n      className={cn(\n        \"absolute touch-manipulation rounded-full\",\n        orientation === \"horizontal\"\n          ? \"inset-y-0 -right-12 my-auto\"\n          : \"-bottom-12 left-1/2 -translate-x-1/2 rotate-90\",\n        className,\n      )}\n      isDisabled={!canScrollNext}\n      onPress={scrollNext}\n      {...props}\n    >\n      <ChevronRightIcon />\n      <span className=\"sr-only\">{labels.next}</span>\n    </Button>\n  );\n}\n\n/**\n * Pauses and resumes `autoplay`. Required whenever the carousel moves on its\n * own (WCAG 2.2.2); it isn't positioned, place it where it fits.\n */\nfunction CarouselAutoplayToggle({\n  className,\n  variant = \"outline\",\n  size = \"icon-sm\",\n  ...props\n}: React.ComponentProps<typeof Button>) {\n  const { isPlaying, setPlaying, labels, registerAutoplayToggle } = useCarousel();\n\n  React.useEffect(registerAutoplayToggle, [registerAutoplayToggle]);\n\n  return (\n    <Button\n      data-slot=\"carousel-autoplay-toggle\"\n      variant={variant}\n      size={size}\n      className={cn(\"touch-manipulation rounded-full\", className)}\n      onPress={() => setPlaying(!isPlaying)}\n      {...props}\n    >\n      {isPlaying ? <PauseIcon /> : <PlayIcon />}\n      <span className=\"sr-only\">{isPlaying ? labels.pause : labels.start}</span>\n    </Button>\n  );\n}\n\n/**\n * One dot per slide, the current one marked; pressing a dot goes to its\n * slide. Not positioned, like the toggle.\n */\nfunction CarouselDots({ className, ...props }: React.ComponentProps<\"div\">) {\n  const { selectedIndex, slideCount, scrollTo, labels } = useCarousel();\n\n  return (\n    <div\n      data-slot=\"carousel-dots\"\n      className={cn(\"flex items-center justify-center gap-1\", className)}\n      {...props}\n    >\n      {Array.from({ length: slideCount }, (_, index) => (\n        // A native button: react-aria's Button doesn't forward `aria-current`.\n        <button\n          // biome-ignore lint/suspicious/noArrayIndexKey: one dot per scroll snap, the index is its identity\n          key={index}\n          type=\"button\"\n          data-slot=\"carousel-dot\"\n          aria-label={labels.goToSlide(index + 1, slideCount)}\n          aria-current={index === selectedIndex || undefined}\n          onClick={() => scrollTo(index)}\n          className=\"group/carousel-dot flex size-5 items-center justify-center rounded-full outline-none hover:bg-accent focus-visible:ring-3 focus-visible:ring-ring/50\"\n        >\n          <span\n            aria-hidden=\"true\"\n            className=\"size-2 rounded-full bg-foreground/20 transition-all group-aria-current/carousel-dot:w-4 group-aria-current/carousel-dot:bg-primary\"\n          />\n        </button>\n      ))}\n    </div>\n  );\n}\n\nexport type { CarouselI18n, CarouselLabels };\nexport {\n  Carousel,\n  type CarouselApi,\n  CarouselAutoplayToggle,\n  CarouselContent,\n  CarouselDots,\n  CarouselItem,\n  CarouselNext,\n  CarouselPrevious,\n  useCarousel,\n};\n",
      "type": "registry:component",
      "target": "components/fma-ds/carousel.tsx"
    }
  ],
  "meta": {
    "status": "stable",
    "source": "customized",
    "license": "MIT",
    "tags": [
      "data-display",
      "carousel",
      "slider",
      "embla"
    ],
    "contentHash": "db3fae44db69"
  },
  "categories": [
    "data-display"
  ],
  "type": "registry:ui"
}