{
  "schemaVersion": 1,
  "name": "carousel",
  "useFor": "Use for a few related items people browse one at a time (merchant cards, screenshots of a checkout). Keep Previous and Next visible; use setApi to show the current slide. Set opts.loop to wrap around and orientation=\"vertical\" for a column.",
  "constraints": [
    "Auto-playing slides",
    "Hiding important content in later slides",
    "A carousel for text people must read in full"
  ],
  "documentation": "",
  "source": "Extension from opencommerce-agentcard-web src/app/globals.css (.win, .btn-secondary)",
  "exports": [
    "Carousel",
    "CarouselContent",
    "CarouselItem",
    "CarouselNext",
    "CarouselPrevious",
    "useCarousel",
    "CarouselApi"
  ],
  "client": true,
  "docsUrl": "https://opencommerce-ds.vercel.app/components/carousel",
  "markdownUrl": "https://opencommerce-ds.vercel.app/r/carousel.md",
  "examples": [
    {
      "path": "examples/carousel.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { Badge } from \"@/components/ui/badge\";\nimport { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious, type CarouselApi } from \"@/components/ui/carousel\";\nimport { Window } from \"@/components/ui/window\";\n\nconst MERCHANTS = [\n  { name: \"Northwind Outfitters\", host: \"northwind.example\", checkouts: \"1,284\", status: \"success\" as const, label: \"Operational\" },\n  { name: \"Contoso Coffee\", host: \"contoso.example\", checkouts: \"642\", status: \"success\" as const, label: \"Operational\" },\n  { name: \"Fabrikam Home\", host: \"fabrikam.example\", checkouts: \"318\", status: \"warning\" as const, label: \"Degraded\" },\n  { name: \"Tailspin Toys\", host: \"tailspin.example\", checkouts: \"97\", status: \"success\" as const, label: \"Operational\" },\n  { name: \"Litware Books\", host: \"litware.example\", checkouts: \"1,016\", status: \"success\" as const, label: \"Operational\" },\n];\n\nfunction Card({ m }: { m: (typeof MERCHANTS)[number] }) {\n  return (\n    <Window name={m.host} bodyClassName=\"grid gap-3 p-5\">\n      <div className=\"flex items-start justify-between gap-3\">\n        <p className=\"font-display text-h5 font-medium tracking-tight text-fg\">{m.name}</p>\n        <Badge variant={m.status}>{m.label}</Badge>\n      </div>\n      <p className=\"text-body-sm text-fg-muted\">\n        <span className=\"font-mono text-fg tabular-nums\">{m.checkouts}</span> checkouts today\n      </p>\n    </Window>\n  );\n}\n\nexport function Site() {\n  const [api, setApi] = React.useState<CarouselApi>();\n  const [index, setIndex] = React.useState(0);\n  React.useEffect(() => {\n    if (!api) return;\n    const update = (a: CarouselApi) => setIndex(a.selectedScrollSnap());\n    api.on(\"select\", update);\n    return () => api.off(\"select\", update);\n  }, [api]);\n  return (\n    <div className=\"mx-auto grid w-full max-w-80 gap-3 sm:max-w-96\">\n      <Carousel setApi={setApi} aria-label=\"Merchants\">\n        <CarouselContent>\n          {MERCHANTS.map((m) => (\n            <CarouselItem key={m.host}>\n              <Card m={m} />\n            </CarouselItem>\n          ))}\n        </CarouselContent>\n        <CarouselPrevious className=\"max-sm:hidden\" />\n        <CarouselNext className=\"max-sm:hidden\" />\n      </Carousel>\n      <p className=\"text-center font-mono text-bar text-fg-muted\">\n        Merchant {index + 1} of {MERCHANTS.length}\n      </p>\n    </div>\n  );\n}\n\nexport function Sizes() {\n  return (\n    <div className=\"mx-auto w-full max-w-160 px-12 max-sm:px-0\">\n      <Carousel opts={{ align: \"start\" }} aria-label=\"Merchants\">\n        <CarouselContent>\n          {MERCHANTS.map((m) => (\n            <CarouselItem key={m.host} className=\"basis-full sm:basis-1/2\">\n              <Card m={m} />\n            </CarouselItem>\n          ))}\n        </CarouselContent>\n        <CarouselPrevious className=\"max-sm:hidden\" />\n        <CarouselNext className=\"max-sm:hidden\" />\n      </Carousel>\n    </div>\n  );\n}\n\nexport function Vertical() {\n  return (\n    <div className=\"mx-auto grid w-full max-w-80 justify-items-center py-12\">\n      <Carousel orientation=\"vertical\" opts={{ align: \"start\" }} className=\"w-full\" aria-label=\"Merchants\">\n        <CarouselContent className=\"h-72\">\n          {MERCHANTS.map((m) => (\n            <CarouselItem key={m.host} className=\"basis-1/2\">\n              <Card m={m} />\n            </CarouselItem>\n          ))}\n        </CarouselContent>\n        <CarouselPrevious />\n        <CarouselNext />\n      </Carousel>\n    </div>\n  );\n}\n",
      "exports": [
        "Site",
        "Sizes",
        "Vertical"
      ],
      "requires": [
        "carousel",
        "badge",
        "window"
      ],
      "packages": [],
      "previews": [
        {
          "name": "Site",
          "url": "https://opencommerce-ds.vercel.app/preview/carousel/Site"
        },
        {
          "name": "Sizes",
          "url": "https://opencommerce-ds.vercel.app/preview/carousel/Sizes"
        },
        {
          "name": "Vertical",
          "url": "https://opencommerce-ds.vercel.app/preview/carousel/Vertical"
        }
      ]
    }
  ],
  "type": "ui",
  "title": "Carousel",
  "description": "A row of slides that snap into place, with white keys to step through them.",
  "surface": "site",
  "kinds": [],
  "requires": [
    "theme",
    "utils"
  ],
  "packages": [
    "lucide-react"
  ],
  "files": [
    {
      "path": "components/ui/carousel.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { ArrowLeft, ArrowRight } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\n// A row of slides that snap into place, drawn from the brand's windows and keys. Slides scroll natively (touch,\n// trackpad and wheel) and snap; the previous and next keys are the white key (a 16 percent ink edge, 8px corners) at\n// 32px with an ink arrow, outside the row, that takes the mist under the pointer and shrinks to 0.97 when pressed.\n// Arrow keys move a slide when the carousel has focus. The scroll is smooth (the browser's own easing) unless reduced\n// motion is set.\n\ntype CarouselEvent = \"select\" | \"reInit\";\ntype CarouselApi = {\n  scrollPrev: () => void;\n  scrollNext: () => void;\n  scrollTo: (index: number) => void;\n  canScrollPrev: () => boolean;\n  canScrollNext: () => boolean;\n  selectedScrollSnap: () => number;\n  scrollSnapList: () => number[];\n  on: (event: CarouselEvent, callback: (api: CarouselApi) => void) => void;\n  off: (event: CarouselEvent, callback: (api: CarouselApi) => void) => void;\n};\ntype CarouselOptions = { loop?: boolean; align?: \"start\" | \"center\" | \"end\" };\n\ntype CarouselProps = {\n  opts?: CarouselOptions;\n  orientation?: \"horizontal\" | \"vertical\";\n  setApi?: (api: CarouselApi) => void;\n};\n\ntype CarouselContextProps = {\n  viewport: React.RefObject<HTMLDivElement | null>;\n  orientation: \"horizontal\" | \"vertical\";\n  align: \"start\" | \"center\" | \"end\";\n  scrollPrev: () => void;\n  scrollNext: () => void;\n  canScrollPrev: boolean;\n  canScrollNext: boolean;\n  update: () => void;\n};\n\nconst CarouselContext = React.createContext<CarouselContextProps | null>(null);\n\nfunction useCarousel() {\n  const context = React.useContext(CarouselContext);\n  if (!context) throw new Error(\"useCarousel must be used within a <Carousel />\");\n  return context;\n}\n\nfunction Carousel({ orientation = \"horizontal\", opts, setApi, className, children, ...props }: React.ComponentProps<\"div\"> & CarouselProps) {\n  const viewport = React.useRef<HTMLDivElement | null>(null);\n  const [edges, setEdges] = React.useState({ prev: false, next: false, index: 0 });\n  const listeners = React.useRef<Record<CarouselEvent, Set<(api: CarouselApi) => void>>>({ select: new Set(), reInit: new Set() });\n  const vertical = orientation === \"vertical\";\n  const loop = !!opts?.loop;\n\n  const slides = React.useCallback(() => Array.from(viewport.current?.querySelectorAll<HTMLElement>(\":scope > [data-slot=carousel-content] > [data-slot=carousel-item]\") ?? []), []);\n  const offsetOf = React.useCallback((el: HTMLElement) => (vertical ? el.offsetTop : el.offsetLeft), [vertical]);\n  const current = React.useCallback(() => {\n    const el = viewport.current;\n    if (!el) return 0;\n    const pos = vertical ? el.scrollTop : el.scrollLeft;\n    const list = slides();\n    let best = 0;\n    list.forEach((s, i) => {\n      if (Math.abs(offsetOf(s) - pos) < Math.abs(offsetOf(list[best]) - pos)) best = i;\n    });\n    return best;\n  }, [offsetOf, slides, vertical]);\n\n  const scrollTo = React.useCallback((index: number) => {\n    const el = viewport.current;\n    const list = slides();\n    if (!el || !list.length) return;\n    const i = loop ? (index + list.length) % list.length : Math.max(0, Math.min(list.length - 1, index));\n    const reduce = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n    el.scrollTo({ [vertical ? \"top\" : \"left\"]: offsetOf(list[i]), behavior: reduce ? \"auto\" : \"smooth\" });\n  }, [loop, offsetOf, slides, vertical]);\n\n  const api = React.useMemo<CarouselApi>(() => {\n    const self: CarouselApi = {\n      scrollPrev: () => scrollTo(current() - 1),\n      scrollNext: () => scrollTo(current() + 1),\n      scrollTo,\n      canScrollPrev: () => {\n        const el = viewport.current;\n        return !!el && (loop || (vertical ? el.scrollTop : el.scrollLeft) > 1);\n      },\n      canScrollNext: () => {\n        const el = viewport.current;\n        if (!el) return false;\n        return loop || (vertical ? el.scrollTop + el.clientHeight < el.scrollHeight - 1 : el.scrollLeft + el.clientWidth < el.scrollWidth - 1);\n      },\n      selectedScrollSnap: current,\n      scrollSnapList: () => slides().map((_, i) => i),\n      on: (event, cb) => void listeners.current[event].add(cb),\n      off: (event, cb) => void listeners.current[event].delete(cb),\n    };\n    return self;\n  }, [current, loop, scrollTo, slides, vertical]);\n\n  // The last slide listeners heard about: they are told outside React's state updates, from the scroll event.\n  const announced = React.useRef(0);\n  const update = React.useCallback(() => {\n    const next = { prev: api.canScrollPrev(), next: api.canScrollNext(), index: api.selectedScrollSnap() };\n    setEdges((e) => (e.prev === next.prev && e.next === next.next && e.index === next.index ? e : next));\n    if (announced.current !== next.index) {\n      announced.current = next.index;\n      listeners.current.select.forEach((cb) => cb(api));\n    }\n  }, [api]);\n\n  React.useEffect(() => {\n    setApi?.(api);\n    const el = viewport.current;\n    if (!el) return;\n    update();\n    const observer = new ResizeObserver(() => {\n      update();\n      listeners.current.reInit.forEach((cb) => cb(api));\n    });\n    observer.observe(el);\n    el.addEventListener(\"scroll\", update, { passive: true });\n    return () => {\n      observer.disconnect();\n      el.removeEventListener(\"scroll\", update);\n    };\n  }, [api, setApi, update]);\n\n  const onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n    const back = vertical ? \"ArrowUp\" : \"ArrowLeft\";\n    const forward = vertical ? \"ArrowDown\" : \"ArrowRight\";\n    if (event.key === back) {\n      event.preventDefault();\n      api.scrollPrev();\n    } else if (event.key === forward) {\n      event.preventDefault();\n      api.scrollNext();\n    }\n  };\n\n  return (\n    <CarouselContext.Provider value={{ viewport, orientation, align: opts?.align ?? \"start\", scrollPrev: api.scrollPrev, scrollNext: api.scrollNext, canScrollPrev: edges.prev, canScrollNext: edges.next, update }}>\n      <div onKeyDownCapture={onKeyDown} className={cn(\"relative min-w-0\", className)} role=\"region\" aria-roledescription=\"carousel\" data-slot=\"carousel\" data-orientation={orientation} {...props}>\n        {children}\n      </div>\n    </CarouselContext.Provider>\n  );\n}\n\nfunction CarouselContent({ className, ...props }: React.ComponentProps<\"div\">) {\n  const { viewport, orientation } = useCarousel();\n  const vertical = orientation === \"vertical\";\n  return (\n    <div\n      ref={viewport}\n      data-slot=\"carousel-viewport\"\n      className={cn(\"overflow-auto overscroll-contain [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\", vertical ? \"h-full snap-y snap-mandatory\" : \"snap-x snap-mandatory\")}\n    >\n      <div data-slot=\"carousel-content\" className={cn(\"flex\", vertical ? \"-mt-4 flex-col\" : \"-ml-4\", className)} {...props} />\n    </div>\n  );\n}\n\nfunction CarouselItem({ className, ...props }: React.ComponentProps<\"div\">) {\n  const { orientation, align } = useCarousel();\n  return (\n    <div\n      role=\"group\"\n      aria-roledescription=\"slide\"\n      data-slot=\"carousel-item\"\n      className={cn(\"min-w-0 shrink-0 grow-0 basis-full\", orientation === \"horizontal\" ? \"pl-4\" : \"pt-4\", align === \"center\" ? \"snap-center\" : align === \"end\" ? \"snap-end\" : \"snap-start\", className)}\n      {...props}\n    />\n  );\n}\n\nconst ARROW =\n  \"absolute inline-flex size-8 items-center justify-center rounded-control border border-hairline bg-surface text-fg outline-none transition-[background-color,opacity,scale] duration-160 ease-out hover:bg-subtle active:scale-97 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-focus-ring disabled:pointer-events-none disabled:opacity-40 motion-reduce:transition-[background-color,opacity] motion-reduce:active:scale-100 [&_svg]:size-3.5\";\n\nfunction CarouselPrevious({ className, ...props }: React.ComponentProps<\"button\">) {\n  const { orientation, scrollPrev, canScrollPrev } = useCarousel();\n  return (\n    <button\n      type=\"button\"\n      data-slot=\"carousel-previous\"\n      disabled={!canScrollPrev}\n      onClick={scrollPrev}\n      className={cn(ARROW, orientation === \"horizontal\" ? \"top-1/2 -left-12 -translate-y-1/2\" : \"-top-12 left-1/2 -translate-x-1/2 rotate-90\", className)}\n      {...props}\n    >\n      <ArrowLeft strokeWidth={1.5} />\n      <span className=\"sr-only\">Previous slide</span>\n    </button>\n  );\n}\n\nfunction CarouselNext({ className, ...props }: React.ComponentProps<\"button\">) {\n  const { orientation, scrollNext, canScrollNext } = useCarousel();\n  return (\n    <button\n      type=\"button\"\n      data-slot=\"carousel-next\"\n      disabled={!canScrollNext}\n      onClick={scrollNext}\n      className={cn(ARROW, orientation === \"horizontal\" ? \"top-1/2 -right-12 -translate-y-1/2\" : \"-bottom-12 left-1/2 -translate-x-1/2 rotate-90\", className)}\n      {...props}\n    >\n      <ArrowRight strokeWidth={1.5} />\n      <span className=\"sr-only\">Next slide</span>\n    </button>\n  );\n}\n\nexport { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious, useCarousel };\nexport type { CarouselApi };\n"
    }
  ]
}
