{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "carousel",
  "title": "Carousel",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A swipeable, keyboard-operable carousel on Embla with slide labels, dot pagination, vertical mode, loop, and autoplay that pauses on hover, focus and reduced motion.",
  "dependencies": [
    "embla-carousel-react@^8",
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/motion-presets.json",
    "https://ui.ballmac.com/r/i18n.json",
    "https://ui.ballmac.com/r/direction.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/carousel.tsx",
      "content": "// Ballmac UI: Carousel. https://ui.ballmac.com/components/carousel\n// Based on shadcn/ui Carousel (MIT, Copyright (c) 2023 shadcn) on Embla Carousel (MIT, Copyright (c) 2019 David Jerleke), adding slide labels, dot pagination, autoplay with a pause control, and reduced-motion handling.\n\"use client\";\n\nimport * as React from \"react\";\nimport useEmblaCarousel, { type UseEmblaCarouselType } from \"embla-carousel-react\";\nimport { ArrowLeft, ArrowRight, Pause, Play } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useReducedMotionSafe } from \"@/lib/ballmac/motion\";\nimport { useDirection } from \"@/lib/ballmac/direction\"\nimport { useMessages } from \"@/lib/ballmac/i18n\";\n\ntype CarouselApi = UseEmblaCarouselType[1];\ntype CarouselOptions = NonNullable<Parameters<typeof useEmblaCarousel>[0]>;\n\ntype CarouselContextValue = {\n  viewportRef: UseEmblaCarouselType[0];\n  api: CarouselApi;\n  orientation: \"horizontal\" | \"vertical\";\n  scrollPrev: () => void;\n  scrollNext: () => void;\n  scrollTo: (index: number) => void;\n  canScrollPrev: boolean;\n  canScrollNext: boolean;\n  selectedIndex: number;\n  count: number;\n  playing: boolean;\n  autoplay: boolean;\n  setPlaying: (playing: boolean) => void;\n};\nconst CarouselContext = React.createContext<CarouselContextValue | null>(null);\nconst SlideContext = React.createContext<{ index: number; count: number } | null>(null);\n\nfunction useCarousel() {\n  const context = React.useContext(CarouselContext);\n  if (!context) throw new Error(\"useCarousel must be used within <Carousel>\");\n  return context;\n}\n\ntype CarouselProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  children?: React.ReactNode;\n  /** Embla options, for example `{ loop: true, align: \"start\" }`. */\n  opts?: CarouselOptions;\n  /** Direction of travel. */\n  orientation?: \"horizontal\" | \"vertical\";\n  /** Receives the Embla API once it is ready, for custom controls. */\n  setApi?: (api: NonNullable<CarouselApi>) => void;\n  /** Advance on a timer, in milliseconds (`true` = 5000). Pauses on hover and focus, and never runs under reduced motion. Pair it with `CarouselPlayPause`. */\n  autoplay?: boolean | number;\n  /** Accessible name of the carousel region. */\n  label?: string;\n};\n\n/** A swipeable, keyboard-operable slider of slides. Compose with CarouselContent, CarouselItem and the controls. */\nfunction Carousel({\n  orientation = \"horizontal\",\n  opts,\n  setApi,\n  autoplay = false,\n  label,\n  className,\n  children,\n  onKeyDownCapture,\n  onMouseEnter,\n  onMouseLeave,\n  onFocus,\n  onBlur,\n  ...props\n}: CarouselProps) {\n  const msg = useMessages()\n  label ??= msg(\"carousel.label\", \"Carousel\")\n  const reduce = useReducedMotionSafe();\n  const dir = useDirection();\n  const [viewportRef, api] = useEmblaCarousel(\n    { ...opts, axis: orientation === \"horizontal\" ? \"x\" : \"y\", direction: dir, ...(reduce ? { duration: 0 } : {}) },\n  );\n  const [canScrollPrev, setCanScrollPrev] = React.useState(false);\n  const [canScrollNext, setCanScrollNext] = React.useState(false);\n  const [selectedIndex, setSelectedIndex] = React.useState(0);\n  const [count, setCount] = React.useState(0);\n  const [userPlaying, setUserPlaying] = React.useState(true);\n  const [hovered, setHovered] = React.useState(false);\n  const [focused, setFocused] = React.useState(false);\n\n  React.useEffect(() => {\n    if (!api) return;\n    const update = () => {\n      setCanScrollPrev(api.canScrollPrev());\n      setCanScrollNext(api.canScrollNext());\n      setSelectedIndex(api.selectedScrollSnap());\n      setCount(api.scrollSnapList().length);\n    };\n    update();\n    api.on(\"select\", update).on(\"reInit\", update);\n    setApi?.(api);\n    return () => {\n      api.off(\"select\", update).off(\"reInit\", update);\n    };\n  }, [api, setApi]);\n\n  const autoplayMs = autoplay === true ? 5000 : autoplay || 0;\n  const running = !!autoplayMs && !reduce && userPlaying && !hovered && !focused;\n  React.useEffect(() => {\n    if (!api || !running) return;\n    const id = window.setInterval(() => {\n      if (document.hidden) return;\n      if (api.canScrollNext()) api.scrollNext();\n      else api.scrollTo(0);\n    }, autoplayMs);\n    return () => window.clearInterval(id);\n  }, [api, running, autoplayMs]);\n\n  const value: CarouselContextValue = {\n    viewportRef,\n    api,\n    orientation,\n    scrollPrev: () => api?.scrollPrev(),\n    scrollNext: () => api?.scrollNext(),\n    scrollTo: (i) => api?.scrollTo(i),\n    canScrollPrev,\n    canScrollNext,\n    selectedIndex,\n    count,\n    playing: !!autoplayMs && !reduce && userPlaying,\n    autoplay: !!autoplayMs && !reduce,\n    setPlaying: setUserPlaying,\n  };\n\n  return (\n    <CarouselContext.Provider value={value}>\n      <div\n        data-slot=\"carousel\"\n        role=\"region\"\n        aria-roledescription=\"carousel\"\n        aria-label={label}\n        className={cn(\"relative min-w-0\", className)}\n        onKeyDownCapture={(event) => {\n          onKeyDownCapture?.(event);\n          if (event.defaultPrevented) return;\n          const target = event.target as HTMLElement;\n          if (target.closest(\"input, textarea, select, [contenteditable=true]\")) return;\n          const prev = orientation === \"horizontal\" ? (dir === \"rtl\" ? \"ArrowRight\" : \"ArrowLeft\") : \"ArrowUp\";\n          const next = orientation === \"horizontal\" ? (dir === \"rtl\" ? \"ArrowLeft\" : \"ArrowRight\") : \"ArrowDown\";\n          if (event.key === prev) {\n            event.preventDefault();\n            api?.scrollPrev();\n          } else if (event.key === next) {\n            event.preventDefault();\n            api?.scrollNext();\n          }\n        }}\n        onMouseEnter={(e) => {\n          onMouseEnter?.(e);\n          setHovered(true);\n        }}\n        onMouseLeave={(e) => {\n          onMouseLeave?.(e);\n          setHovered(false);\n        }}\n        onFocus={(e) => {\n          onFocus?.(e);\n          setFocused(true);\n        }}\n        onBlur={(e) => {\n          onBlur?.(e);\n          setFocused(false);\n        }}\n        {...props}\n      >\n        {children}\n      </div>\n    </CarouselContext.Provider>\n  );\n}\n\ntype CarouselContentProps = React.ComponentProps<\"div\"> & {\n  /** Classes for the clipping viewport around the track. */\n  viewportClassName?: string;\n};\nfunction CarouselContent({ className, viewportClassName, children, ...props }: CarouselContentProps) {\n  const { viewportRef, orientation, autoplay, playing } = useCarousel();\n  const slides = React.Children.toArray(children);\n  return (\n    <div\n      ref={viewportRef}\n      data-slot=\"carousel-viewport\"\n      className={cn(\"overflow-hidden\", viewportClassName)}\n    >\n      <div\n        data-slot=\"carousel-content\"\n        aria-live={autoplay && playing ? \"off\" : \"polite\"}\n        className={cn(\"flex\", orientation === \"horizontal\" ? \"-ms-4\" : \"-mt-4 flex-col\", className)}\n        {...props}\n      >\n        {slides.map((child, index) => (\n          <SlideContext.Provider key={index} value={{ index, count: slides.length }}>\n            {child}\n          </SlideContext.Provider>\n        ))}\n      </div>\n    </div>\n  );\n}\n\ntype CarouselItemProps = React.ComponentProps<\"div\">;\nfunction CarouselItem({ className, ...props }: CarouselItemProps) {\n  const { orientation } = useCarousel();\n  const slide = React.useContext(SlideContext);\n  return (\n    <div\n      role=\"group\"\n      aria-roledescription=\"slide\"\n      aria-label={slide ? `${slide.index + 1} of ${slide.count}` : undefined}\n      data-slot=\"carousel-item\"\n      className={cn(\n        \"min-w-0 shrink-0 grow-0 basis-full\",\n        orientation === \"horizontal\" ? \"ps-4\" : \"pt-4\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nconst controlClass =\n  \"inline-flex size-9 items-center justify-center rounded-full border bg-background/90 text-foreground shadow-sm outline-none backdrop-blur transition-[background-color,opacity,box-shadow] hover:bg-accent focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-40 [&_svg]:size-4\";\n\ntype CarouselPreviousProps = React.ComponentProps<\"button\">;\nfunction CarouselPrevious({ className, ...props }: CarouselPreviousProps) {\n  const msg = useMessages()\n  const { orientation, scrollPrev, canScrollPrev } = useCarousel();\n  return (\n    <button\n      type=\"button\"\n      data-slot=\"carousel-previous\"\n      aria-label={msg(\"carousel.previousSlide\", \"Previous slide\")}\n      disabled={!canScrollPrev}\n      onClick={scrollPrev}\n      className={cn(\n        controlClass,\n        \"absolute z-10\",\n        orientation === \"horizontal\"\n          ? \"top-1/2 start-3 -translate-y-1/2\"\n          : \"top-3 left-1/2 -translate-x-1/2 rotate-90\",\n        className,\n      )}\n      {...props}\n    >\n      <ArrowLeft aria-hidden=\"true\" className={orientation === \"horizontal\" ? \"rtl:rotate-180\" : undefined} />\n    </button>\n  );\n}\n\ntype CarouselNextProps = React.ComponentProps<\"button\">;\nfunction CarouselNext({ className, ...props }: CarouselNextProps) {\n  const msg = useMessages()\n  const { orientation, scrollNext, canScrollNext } = useCarousel();\n  return (\n    <button\n      type=\"button\"\n      data-slot=\"carousel-next\"\n      aria-label={msg(\"carousel.nextSlide\", \"Next slide\")}\n      disabled={!canScrollNext}\n      onClick={scrollNext}\n      className={cn(\n        controlClass,\n        \"absolute z-10\",\n        orientation === \"horizontal\"\n          ? \"top-1/2 end-3 -translate-y-1/2\"\n          : \"bottom-3 left-1/2 -translate-x-1/2 rotate-90\",\n        className,\n      )}\n      {...props}\n    >\n      <ArrowRight aria-hidden=\"true\" className={orientation === \"horizontal\" ? \"rtl:rotate-180\" : undefined} />\n    </button>\n  );\n}\n\ntype CarouselDotsProps = React.ComponentProps<\"div\">;\n/** Pagination dots. The active dot stretches into a pill. Each dot is a labelled button. */\nfunction CarouselDots({ className, ...props }: CarouselDotsProps) {\n  const msg = useMessages()\n  const { count, selectedIndex, scrollTo } = useCarousel();\n  if (count < 2) return null;\n  return (\n    <div\n      data-slot=\"carousel-dots\"\n      role=\"group\"\n      aria-label={msg(\"carousel.chooseSlide\", \"Choose slide\")}\n      className={cn(\"flex items-center justify-center gap-1\", className)}\n      {...props}\n    >\n      {Array.from({ length: count }, (_, i) => (\n        <button\n          key={i}\n          type=\"button\"\n          aria-label={msg(\"carousel.goToSlide\", \"Go to slide {n}\", { n: i + 1 })}\n          aria-current={i === selectedIndex ? \"true\" : undefined}\n          onClick={() => scrollTo(i)}\n          className=\"group/dot inline-flex h-6 min-w-6 items-center justify-center rounded-full outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50\"\n        >\n          <span\n            className={cn(\n              \"block h-1.5 rounded-full transition-[width,background-color] duration-300 motion-reduce:transition-none\",\n              i === selectedIndex\n                ? \"w-5 bg-primary\"\n                : \"w-1.5 bg-muted-foreground/40 group-hover/dot:bg-muted-foreground/70\",\n            )}\n          />\n        </button>\n      ))}\n    </div>\n  );\n}\n\ntype CarouselPlayPauseProps = React.ComponentProps<\"button\">;\n/** Pause and resume autoplay. Renders nothing when `autoplay` is off or reduced motion is on. */\nfunction CarouselPlayPause({ className, ...props }: CarouselPlayPauseProps) {\n  const msg = useMessages()\n  const { autoplay, playing, setPlaying } = useCarousel();\n  if (!autoplay) return null;\n  return (\n    <button\n      type=\"button\"\n      data-slot=\"carousel-play-pause\"\n      aria-label={playing ? msg(\"carousel.pauseAutoplay\", \"Pause autoplay\") : msg(\"carousel.startAutoplay\", \"Start autoplay\")}\n      onClick={() => setPlaying(!playing)}\n      className={cn(controlClass, \"size-8\", className)}\n      {...props}\n    >\n      {playing ? <Pause aria-hidden=\"true\" /> : <Play aria-hidden=\"true\" />}\n    </button>\n  );\n}\n\nexport {\n  Carousel,\n  CarouselContent,\n  CarouselItem,\n  CarouselPrevious,\n  CarouselNext,\n  CarouselDots,\n  CarouselPlayPause,\n  useCarousel,\n  type CarouselApi,\n  type CarouselProps,\n  type CarouselContentProps,\n  type CarouselItemProps,\n  type CarouselPreviousProps,\n  type CarouselNextProps,\n  type CarouselDotsProps,\n  type CarouselPlayPauseProps,\n};\n",
      "type": "registry:ui",
      "target": "@components/ballmac/carousel.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "slider",
      "swipe",
      "slides",
      "embla"
    ],
    "version": "1.0.0",
    "updated": "2026-09-30",
    "ai": {
      "summary": "Compose Carousel, CarouselContent and CarouselItem with CarouselPrevious, CarouselNext, CarouselDots and CarouselPlayPause. Give Carousel a label.",
      "whenToUse": [
        "Feature highlights and testimonials",
        "Product image galleries",
        "Horizontal lists that overflow"
      ],
      "whenNotToUse": [
        "Content people must read in full; show it all",
        "A marquee of logos; use marquee"
      ],
      "composesWith": [
        "card",
        "button"
      ],
      "a11y": [
        {
          "keys": "ArrowLeft / ArrowRight",
          "action": "Previous or next slide (Up / Down when vertical)"
        },
        {
          "keys": "Tab",
          "action": "Reaches arrows, dots and the pause button"
        },
        {
          "keys": "Swipe / drag",
          "action": "Moves slides with touch or mouse"
        }
      ],
      "customization": [
        "opts (loop, align, slidesToScroll)",
        "orientation",
        "autoplay in ms",
        "item basis classes for several visible slides"
      ]
    },
    "source": {
      "name": "shadcn/ui Carousel",
      "url": "https://github.com/shadcn-ui/ui",
      "license": "MIT",
      "copyright": "Copyright (c) 2023 shadcn",
      "modified": true
    },
    "examples": [
      "carousel-demo",
      "carousel-states"
    ],
    "url": "https://ui.ballmac.com/components/carousel"
  },
  "categories": [
    "data-display"
  ],
  "type": "registry:ui"
}