{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "carousel",
  "type": "registry:component",
  "title": "Carousel",
  "description": "Browses a sequence on a scroll-snap track. Buttons move one slide; the ends feather.",
  "registryDependencies": [
    "https://vlak.dev/r/vlak-base.json",
    "https://vlak.dev/r/inter.json",
    "https://vlak.dev/r/pagination.json",
    "https://vlak.dev/r/vlak-lib.json",
    "https://vlak.dev/r/icons.json"
  ],
  "dependencies": [
    "@stylexjs/stylex"
  ],
  "devDependencies": [
    "@stylexjs/babel-plugin"
  ],
  "docs": "Vlak leaves are StyleX. Compile them with @stylexjs/babel-plugin (Vite: @stylexjs/unplugin, Next: @stylexjs/nextjs-plugin). If you would rather not run a compiler, import @noorddev/vlak-react instead: it ships precompiled with one stylesheet.",
  "files": [
    {
      "path": "vlak/carousel.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as stylex from \"@stylexjs/stylex\";\nimport { vlak, mq } from \"./tokens.stylex\";\nimport { rs } from \"./rs\";\nimport { Icon } from \"./icons\";\n\nexport interface CarouselProps extends React.HTMLAttributes<HTMLDivElement> {\n  /** Name of the carousel region. */\n  \"aria-label\"?: string;\n}\n\nconst fade =\n  \"linear-gradient(to right, transparent 0, black 28px, black calc(100% - 36px), transparent 100%)\";\n\nconst styles = stylex.create({\n  carousel: {\n    position: \"relative\",\n    width: \"100%\",\n    maxWidth: \"100%\",\n  },\n  track: {\n    display: \"flex\",\n    gap: vlak.gutter,\n    overflowX: \"auto\",\n    scrollSnapType: \"x mandatory\",\n    scrollBehavior: {\n      default: \"smooth\",\n      [mq.reduce]: \"auto\",\n    },\n    scrollbarWidth: \"none\",\n    \"::-webkit-scrollbar\": {\n      display: \"none\",\n    },\n    maskImage: fade,\n    paddingTop: 2,\n    paddingBottom: 2,\n    paddingInline: 0,\n    outlineWidth: {\n      default: null,\n      \":focus-visible\": 2,\n    },\n    outlineStyle: {\n      default: null,\n      \":focus-visible\": \"solid\",\n    },\n    outlineColor: {\n      default: null,\n      \":focus-visible\": vlak.ink,\n    },\n    outlineOffset: {\n      default: null,\n      \":focus-visible\": 2,\n    },\n  },\n  nav: {\n    display: \"flex\",\n    gap: {\n      default: \"0.3125rem\",\n      [mq.phone]: \"0.5rem\",\n    },\n    marginTop: {\n      default: \"0.625rem\",\n      [mq.phone]: \"0.875rem\",\n    },\n  },\n  page: {\n    boxSizing: \"border-box\",\n    width: {\n      default: vlak.hit,\n      [mq.phone]: vlak.hit,\n    },\n    height: {\n      default: vlak.hit,\n      [mq.phone]: vlak.hit,\n    },\n    minWidth: {\n      default: null,\n      [mq.phone]: vlak.hit,\n    },\n    minHeight: {\n      default: null,\n      [mq.phone]: vlak.hit,\n    },\n    borderRadius: {\n      default: vlak.radiusSm,\n      [mq.phone]: vlak.radiusSm,\n    },\n    display: \"flex\",\n    alignItems: \"center\",\n    justifyContent: \"center\",\n    fontSize: {\n      default: \"0.8125rem\",\n      [mq.phone]: vlak.controlFs,\n    },\n    color: {\n      default: vlak.gray,\n      [mq.forcedColors]: \"ButtonText\",\n    },\n    borderWidth: vlak.hairline,\n    borderStyle: \"solid\",\n    borderColor: {\n      default: vlak.controlBorder,\n      [mq.forcedColors]: \"ButtonText\",\n    },\n    padding: 0,\n    backgroundColor: \"transparent\",\n    fontFamily: \"inherit\",\n    cursor: \"pointer\",\n    outlineWidth: {\n      default: null,\n      \":focus-visible\": 2,\n    },\n    outlineStyle: {\n      default: null,\n      \":focus-visible\": \"solid\",\n    },\n    outlineColor: {\n      default: null,\n      \":focus-visible\": vlak.ink,\n    },\n    outlineOffset: {\n      default: null,\n      \":focus-visible\": 2,\n    },\n  },\n  icon: {\n    display: \"block\",\n    color: \"inherit\",\n  },\n  slide: {\n    flexShrink: 0,\n    scrollSnapAlign: \"start\",\n    borderRadius: {\n      default: vlak.radiusSm,\n      [mq.phone]: vlak.radiusSm,\n    },\n  },\n});\n\nconst SlideContext = React.createContext<{ index: number; count: number } | null>(null);\n\n/** One slide: a named group, \"n of N\" unless you name it yourself. */\nexport const CarouselSlide = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(function CarouselSlide(\n  { className, style, \"aria-label\": ariaLabel, ...props },\n  ref,\n) {\n  const slot = React.useContext(SlideContext);\n  const sx = rs([\"rs-carousel-slide\", className], styles.slide);\n  return (\n    <div\n      ref={ref}\n      role=\"group\"\n      aria-roledescription=\"slide\"\n      aria-label={ariaLabel ?? (slot ? `${slot.index + 1} of ${slot.count}` : undefined)}\n      {...props}\n      className={sx.className}\n      style={{ ...sx.style, ...style }}\n    />\n  );\n});\n\n/** Native scroll snap; the buttons nudge. The track is the named, focusable carousel region. */\nexport const Carousel = React.forwardRef<HTMLDivElement, CarouselProps>(function Carousel(\n  { className, style, children, \"aria-label\": ariaLabel = \"Carousel\", ...props },\n  ref,\n) {\n  const trackRef = React.useRef<HTMLDivElement>(null);\n  const nudge = (dir: 1 | -1) => {\n    const track = trackRef.current;\n    if (track) track.scrollBy({ left: dir * track.clientWidth * 0.8 });\n  };\n  const root = rs([\"rs-carousel\", className], styles.carousel);\n  const track = rs([\"rs-carousel-track\"], styles.track);\n  const nav = rs([\"rs-carousel-nav\"], styles.nav);\n  const page = rs([\"rs-page\", \"rs-carousel-page\"], styles.page);\n  const icon = rs([\"rs-carousel-icon\"], styles.icon);\n  const slides = React.Children.toArray(children);\n  return (\n    <div ref={ref} {...props} className={root.className} style={{ ...root.style, ...style }}>\n      <div\n        ref={trackRef}\n        role=\"region\"\n        aria-roledescription=\"carousel\"\n        aria-label={ariaLabel}\n        className={track.className}\n        style={track.style}\n        tabIndex={0}\n      >\n        {slides.map((child, index) => (\n          <SlideContext.Provider key={React.isValidElement(child) && child.key != null ? child.key : index} value={{ index, count: slides.length }}>\n            {child}\n          </SlideContext.Provider>\n        ))}\n      </div>\n      <div className={nav.className} style={nav.style}>\n        <button type=\"button\" className={page.className} style={page.style} aria-label=\"Previous\" onClick={() => nudge(-1)}>\n          <Icon name=\"chevron-left\" size={12} className={icon.className} style={icon.style} />\n        </button>\n        <button type=\"button\" className={page.className} style={page.style} aria-label=\"Next\" onClick={() => nudge(1)}>\n          <Icon name=\"chevron-right\" size={12} className={icon.className} style={icon.style} />\n        </button>\n      </div>\n    </div>\n  );\n});\n",
      "type": "registry:component",
      "target": "components/vlak/carousel.tsx"
    },
    {
      "path": "vlak/styles/carousel.css",
      "content": "/* ── carousel: generated from packages/react/src/components/carousel.tsx ── */\n.rs-carousel{position:relative;width:100%;max-width:100%}\n.rs-carousel-track{display:flex;gap:var(--gutter);overflow-x:auto;scroll-snap-type:x mandatory;scroll-behavior:smooth;scrollbar-width:none;mask-image:linear-gradient(to right, transparent 0, black 28px, black calc(100% - 36px), transparent 100%);padding-top:2px;padding-bottom:2px;padding-inline:0}\n.rs-carousel-track::-webkit-scrollbar{display:none}\n.rs-carousel-track:focus-visible{outline-width:2px;outline-style:solid;outline-color:var(--text);outline-offset:2px}\n@media (prefers-reduced-motion: reduce){.rs-carousel-track{scroll-behavior:auto}}\n.rs-carousel-nav{display:flex;gap:0.3125rem;margin-top:0.625rem}\n@media (max-width: 640px){.rs-carousel-nav{gap:0.5rem;margin-top:0.875rem}}\n.rs-page.rs-carousel-page{box-sizing:border-box;width:var(--hit);height:var(--hit);border-radius:var(--radius-sm);display:flex;align-items:center;justify-content:center;font-size:0.8125rem;color:var(--text-secondary);border-width:1px;border-style:solid;border-color:var(--control-border);padding:0;background-color:transparent;font-family:inherit;cursor:pointer}\n.rs-page.rs-carousel-page:focus-visible{outline-width:2px;outline-style:solid;outline-color:var(--text);outline-offset:2px}\n@media (max-width: 640px){.rs-page.rs-carousel-page{width:var(--hit);height:var(--hit);min-width:var(--hit);min-height:var(--hit);border-radius:var(--radius-sm);font-size:var(--control-fs)}}\n@media (forced-colors: active){.rs-page.rs-carousel-page{color:ButtonText;border-color:ButtonText}}\n.rs-carousel-icon{display:block;color:inherit}\n.rs-carousel-slide{flex-shrink:0;scroll-snap-align:start;border-radius:var(--radius-sm)}\n@media (max-width: 640px){.rs-carousel-slide{border-radius:var(--radius-sm)}}\n",
      "type": "registry:file",
      "target": "styles/vlak/carousel.css"
    }
  ],
  "meta": {
    "vlak": {
      "category": "content",
      "classes": [
        "rs-carousel",
        "rs-carousel-track",
        "rs-carousel-slide",
        "rs-carousel-nav",
        "rs-carousel-icon",
        "rs-carousel-page"
      ],
      "snippet": "<div class=\"rs-carousel\"><div class=\"rs-carousel-track\"><div class=\"rs-carousel-slide\">One</div><div class=\"rs-carousel-slide\">Two</div><div class=\"rs-carousel-slide\">Three</div></div><div class=\"rs-carousel-nav\"><button class=\"rs-page\"><svg viewBox=\"0 0 16 16\" width=\"12\" height=\"12\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M10.5 3.75 L5.5 8.25 L10.5 12.75\" vector-effect=\"non-scaling-stroke\"/></svg></button><button class=\"rs-page\"><svg viewBox=\"0 0 16 16\" width=\"12\" height=\"12\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M5.5 3.75 L10.5 8.25 L5.5 12.75\" vector-effect=\"non-scaling-stroke\"/></svg></button></div></div>",
      "cssOnly": false,
      "registryDependencies": [
        "pagination",
        "vlak-lib",
        "icons"
      ],
      "aliases": [
        "Carousel",
        "Slideshow",
        "Embla",
        "Swiper"
      ],
      "example": "import { Carousel, CarouselSlide } from \"@noorddev/vlak-react\";\n\n<Carousel aria-label=\"Case studies\">\n  {cases.map((c) => (\n    <CarouselSlide key={c.id}>\n      <CaseCard {...c} />\n    </CarouselSlide>\n  ))}\n</Carousel>",
      "usage": {
        "use": [
          "A row of cards or images wider than the page, browsed one nudge at a time.",
          "Scroll snap does the alignment; the buttons nudge by 80% of the width."
        ],
        "avoid": [
          "Autoplay or hero sliders; nothing moves on its own.",
          "Essential content only reachable by scrolling; show it in the flow too."
        ]
      },
      "keyboard": [
        {
          "keys": "Tab",
          "does": "Focuses the track, then the previous and next buttons"
        },
        {
          "keys": "Arrow left, Arrow right",
          "does": "Scrolls the focused track"
        },
        {
          "keys": "Enter, Space",
          "does": "Nudges from the buttons"
        }
      ],
      "a11y": [
        "The track is role=\"region\" with aria-roledescription=\"carousel\", named by aria-label, and focusable.",
        "CarouselSlide is role=\"group\" with aria-roledescription=\"slide\", named \"n of N\" unless you pass aria-label.",
        "The buttons are labelled \"Previous\" and \"Next\"."
      ]
    }
  }
}
