{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "carousel",
  "title": "Carousel",
  "description": "Scroll-snap gallery with arrows and dots.",
  "dependencies": [
    "lucide-react@^1.33.0"
  ],
  "registryDependencies": [
    "https://components.exe.xyz/r/icon-button.json",
    "https://components.exe.xyz/r/utils.json"
  ],
  "files": [
    {
      "path": "src/components/ui/carousel.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ChevronLeftIcon, ChevronRightIcon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { IconButton } from \"@/components/ui/icon-button\"\n\ntype CarouselContextValue = {\n  align: \"start\" | \"center\"\n}\n\nconst CarouselContext = React.createContext<CarouselContextValue>({\n  align: \"start\",\n})\n\nfunction prefersReducedMotion() {\n  if (typeof window === \"undefined\") return false\n  return window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n}\n\nfunction Carousel({\n  align = \"start\",\n  gap = 12,\n  showArrows = true,\n  showDots = true,\n  className,\n  children,\n  \"aria-label\": ariaLabel,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  align?: \"start\" | \"center\"\n  gap?: number\n  showArrows?: boolean\n  showDots?: boolean\n  \"aria-label\": string\n}) {\n  const trackRef = React.useRef<HTMLDivElement>(null)\n  const count = React.Children.count(children)\n  const [index, setIndex] = React.useState(0)\n  const [atStart, setAtStart] = React.useState(true)\n  const [atEnd, setAtEnd] = React.useState(false)\n\n  const readPosition = React.useCallback(() => {\n    const track = trackRef.current\n    if (!track) return\n    const items = Array.from(\n      track.querySelectorAll<HTMLElement>(\"[data-slot='carousel-item']\")\n    )\n    if (items.length === 0) return\n\n    const trackRect = track.getBoundingClientRect()\n    const origin =\n      align === \"center\" ? trackRect.left + trackRect.width / 2 : trackRect.left\n\n    let best = 0\n    let bestDist = Number.POSITIVE_INFINITY\n    items.forEach((item, i) => {\n      const rect = item.getBoundingClientRect()\n      const point = align === \"center\" ? rect.left + rect.width / 2 : rect.left\n      const dist = Math.abs(point - origin)\n      if (dist < bestDist) {\n        bestDist = dist\n        best = i\n      }\n    })\n\n    setIndex(best)\n    setAtStart(track.scrollLeft <= 2)\n    setAtEnd(track.scrollLeft + track.clientWidth >= track.scrollWidth - 2)\n  }, [align])\n\n  React.useEffect(() => {\n    const track = trackRef.current\n    if (!track) return\n    readPosition()\n    track.addEventListener(\"scroll\", readPosition, { passive: true })\n    const ro = new ResizeObserver(readPosition)\n    ro.observe(track)\n    return () => {\n      track.removeEventListener(\"scroll\", readPosition)\n      ro.disconnect()\n    }\n  }, [readPosition, count])\n\n  const scrollToIndex = React.useCallback(\n    (next: number) => {\n      const track = trackRef.current\n      if (!track) return\n      const items = track.querySelectorAll<HTMLElement>(\n        \"[data-slot='carousel-item']\"\n      )\n      if (next < 0 || next >= items.length) return\n      const item = items[next]\n      const trackRect = track.getBoundingClientRect()\n      const itemRect = item.getBoundingClientRect()\n      const extra =\n        align === \"center\" ? (track.clientWidth - item.clientWidth) / 2 : 0\n      const left = track.scrollLeft + (itemRect.left - trackRect.left) - extra\n      track.scrollTo({\n        left,\n        behavior: prefersReducedMotion() ? \"auto\" : \"smooth\",\n      })\n    },\n    [align]\n  )\n\n  const go = (delta: number) => {\n    const next = Math.min(Math.max(index + delta, 0), count - 1)\n    scrollToIndex(next)\n  }\n\n  return (\n    <CarouselContext.Provider value={{ align }}>\n      <div\n        role=\"region\"\n        aria-roledescription=\"carousel\"\n        aria-label={ariaLabel}\n        data-slot=\"carousel\"\n        className={cn(\"flex w-full flex-col gap-3\", className)}\n        {...props}\n      >\n        <div className=\"relative\">\n          {showArrows ? (\n            <IconButton\n              icon={ChevronLeftIcon}\n              size=\"small\"\n              variant=\"secondary\"\n              aria-label=\"Previous slide\"\n              disabled={atStart}\n              className=\"absolute top-1/2 left-2 z-10 -translate-y-1/2\"\n              onClick={() => go(-1)}\n            />\n          ) : null}\n          <div\n            ref={trackRef}\n            tabIndex={0}\n            data-slot=\"carousel-track\"\n            onKeyDown={(event) => {\n              if (event.key === \"ArrowLeft\") {\n                event.preventDefault()\n                go(-1)\n              }\n              if (event.key === \"ArrowRight\") {\n                event.preventDefault()\n                go(1)\n              }\n            }}\n            className=\"flex snap-x snap-mandatory [scrollbar-width:none] overflow-x-auto outline-none focus-visible:ring-3 focus-visible:ring-ring/50 [&::-webkit-scrollbar]:hidden\"\n            style={{ gap }}\n          >\n            {React.Children.map(children, (child, i) => {\n              if (!React.isValidElement<React.ComponentProps<\"div\">>(child)) {\n                return child\n              }\n              return React.cloneElement(child, {\n                \"aria-label\": `${i + 1} of ${count}`,\n              })\n            })}\n          </div>\n          {showArrows ? (\n            <IconButton\n              icon={ChevronRightIcon}\n              size=\"small\"\n              variant=\"secondary\"\n              aria-label=\"Next slide\"\n              disabled={atEnd}\n              className=\"absolute top-1/2 right-2 z-10 -translate-y-1/2\"\n              onClick={() => go(1)}\n            />\n          ) : null}\n        </div>\n        {showDots && count > 1 ? (\n          <div\n            data-slot=\"carousel-dots\"\n            className=\"flex items-center justify-center gap-1.5\"\n          >\n            {Array.from({ length: count }, (_, i) => (\n              <button\n                key={i}\n                type=\"button\"\n                aria-label={`Go to slide ${i + 1}`}\n                aria-current={i === index ? \"true\" : undefined}\n                onClick={() => scrollToIndex(i)}\n                className={cn(\n                  \"size-2 rounded-full border border-border transition-colors outline-none focus-visible:ring-3 focus-visible:ring-ring/50\",\n                  i === index\n                    ? \"bg-foreground\"\n                    : \"bg-transparent hover:bg-muted\"\n                )}\n              />\n            ))}\n          </div>\n        ) : null}\n      </div>\n    </CarouselContext.Provider>\n  )\n}\n\nfunction CarouselItem({ className, ...props }: React.ComponentProps<\"div\">) {\n  const { align } = React.useContext(CarouselContext)\n\n  return (\n    <div\n      role=\"group\"\n      aria-roledescription=\"slide\"\n      data-slot=\"carousel-item\"\n      className={cn(\n        \"w-full min-w-0 shrink-0\",\n        align === \"center\" ? \"snap-center\" : \"snap-start\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nexport { Carousel, CarouselItem }\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}