{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "carousel-snap",
  "title": "Carousel Snap",
  "description": "A carousel with no carousel library: a native scroll-snap row with buttons that scroll by a page and a position readout.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "@tealmac/carousel-kit"
  ],
  "files": [
    {
      "path": "registry/tealmac/carousel-snap.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ChevronLeft, ChevronRight } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport {\n  isRtlElement,\n  usePrefersReducedMotion,\n  type CarouselItem,\n} from \"@/registry/tealmac/carousel-kit\"\n\nexport interface CarouselSnapProps {\n  items: CarouselItem[]\n  label: string\n  /** Slide width classes. @default \"basis-[80%] sm:basis-[45%]\" */\n  slideClassName?: string\n  className?: string\n}\n\n/**\n * A carousel with no carousel library: a native `scroll-snap` row. The\n * browser does the touch, trackpad and keyboard scrolling; arrows and the\n * position readout are added on top. Lightest option; no drag-to-glide.\n */\nexport function CarouselSnap({\n  items,\n  label,\n  slideClassName = \"basis-[80%] sm:basis-[45%]\",\n  className,\n}: CarouselSnapProps) {\n  const reducedMotion = usePrefersReducedMotion()\n  const scroller = React.useRef<HTMLDivElement>(null)\n  const [edge, setEdge] = React.useState({ start: true, end: false })\n  const [current, setCurrent] = React.useState(0)\n\n  const measure = React.useCallback(() => {\n    const el = scroller.current\n    if (!el) return\n    const offset = Math.abs(el.scrollLeft)\n    const max = el.scrollWidth - el.clientWidth\n    setEdge({ start: offset < 2, end: offset > max - 2 })\n    const first = el.firstElementChild as HTMLElement | null\n    const step = first ? first.offsetWidth || 1 : 1\n    setCurrent(Math.round(offset / step))\n  }, [])\n\n  React.useEffect(() => {\n    measure()\n    const el = scroller.current\n    if (!el) return\n    const observer = new ResizeObserver(measure)\n    observer.observe(el)\n    return () => observer.disconnect()\n  }, [measure])\n\n  const scrollByPage = (direction: 1 | -1) => {\n    const el = scroller.current\n    if (!el) return\n    const rtl = isRtlElement(el)\n    el.scrollBy({\n      left: direction * (rtl ? -1 : 1) * el.clientWidth * 0.9,\n      behavior: reducedMotion ? \"auto\" : \"smooth\",\n    })\n  }\n\n  return (\n    <div\n      role=\"region\"\n      aria-roledescription=\"carousel\"\n      aria-label={label}\n      data-slot=\"carousel-snap\"\n      className={cn(\"relative\", className)}\n    >\n      <div\n        ref={scroller}\n        onScroll={measure}\n        // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex -- a scrollable area must be focusable so keyboard users can scroll it\n        tabIndex={0}\n        aria-label={`${label}, scrollable`}\n        data-slot=\"carousel-snap-scroller\"\n        className=\"focus-visible:ring-ring/50 flex snap-x snap-mandatory [scrollbar-width:thin] gap-3 overflow-x-auto scroll-smooth rounded-lg pb-2 outline-none focus-visible:ring-[3px] motion-reduce:scroll-auto\"\n      >\n        {items.map((item, i) => (\n          <div\n            key={item.id}\n            role=\"group\"\n            aria-roledescription=\"slide\"\n            aria-label={`${i + 1} of ${items.length}`}\n            className={cn(\"shrink-0 snap-start\", slideClassName)}\n          >\n            {item.content}\n          </div>\n        ))}\n      </div>\n      <div className=\"mt-2 flex items-center justify-between\">\n        <p className=\"text-muted-foreground text-sm tabular-nums\">\n          {Math.min(current + 1, items.length)} / {items.length}\n        </p>\n        <div className=\"flex gap-2\">\n          <button\n            type=\"button\"\n            aria-label=\"Previous slides\"\n            disabled={edge.start}\n            onClick={() => scrollByPage(-1)}\n            className=\"bg-background hover:bg-accent focus-visible:ring-ring/50 inline-flex size-9 items-center justify-center rounded-full border focus-visible:ring-[3px] focus-visible:outline-none disabled:opacity-40\"\n          >\n            <ChevronLeft aria-hidden className=\"size-4 rtl:-scale-x-100\" />\n          </button>\n          <button\n            type=\"button\"\n            aria-label=\"Next slides\"\n            disabled={edge.end}\n            onClick={() => scrollByPage(1)}\n            className=\"bg-background hover:bg-accent focus-visible:ring-ring/50 inline-flex size-9 items-center justify-center rounded-full border focus-visible:ring-[3px] focus-visible:outline-none disabled:opacity-40\"\n          >\n            <ChevronRight aria-hidden className=\"size-4 rtl:-scale-x-100\" />\n          </button>\n        </div>\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}