{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tabs",
  "type": "registry:component",
  "title": "Tabs",
  "description": "Switches between related panels. Text labels in one row; active tab has a 1px underline.",
  "registryDependencies": [
    "https://vlak.dev/r/vlak-base.json",
    "https://vlak.dev/r/inter.json",
    "https://vlak.dev/r/vlak-lib.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/tabs.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 { useMergedRefs } from \"./merge-refs\";\n\ninterface TabsContextValue {\n  value: string;\n  setValue: (value: string) => void;\n  idBase: string;\n  /** Panels currently mounted, so a tab only claims aria-controls for one that exists. */\n  panels: ReadonlySet<string>;\n  registerPanel: (value: string) => () => void;\n  registerTab: (value: string, element: HTMLButtonElement, disabled: boolean) => () => void;\n}\n\nconst TabsContext = React.createContext<TabsContextValue | null>(null);\n\nfunction useTabsContext(component: string): TabsContextValue {\n  const ctx = React.useContext(TabsContext);\n  if (!ctx) throw new Error(`<${component}> must be used inside <Tabs>`);\n  return ctx;\n}\n\nconst styles = stylex.create({\n  list: {\n    display: \"flex\",\n    alignItems: {\n      default: \"baseline\",\n      [mq.phone]: \"stretch\",\n    },\n    gap: {\n      default: \"1.375rem\",\n      [mq.phone]: 0,\n    },\n    width: {\n      default: null,\n      [mq.phone]: \"100%\",\n    },\n    maxWidth: {\n      default: \"22.5rem\",\n      [mq.phone]: \"none\",\n    },\n    borderWidth: 0,\n    borderStyle: \"none\",\n    borderBottomWidth: {\n      default: 0,\n      [mq.phone]: vlak.hairline,\n    },\n    borderBottomStyle: {\n      default: \"none\",\n      [mq.phone]: \"solid\",\n    },\n    borderBottomColor: {\n      default: \"transparent\",\n      [mq.phone]: vlak.divider,\n    },\n    boxShadow: \"none\",\n    backgroundColor: \"transparent\",\n  },\n  vertical: {\n    flexDirection: \"column\",\n    alignItems: \"stretch\",\n    gap: 0,\n    borderBottomWidth: 0,\n    borderBottomStyle: \"none\",\n  },\n  tab: {\n    appearance: \"none\",\n    boxSizing: \"border-box\",\n    flexGrow: {\n      default: null,\n      [mq.phone]: 1,\n    },\n    flexShrink: {\n      default: null,\n      [mq.phone]: 1,\n    },\n    flexBasis: {\n      default: null,\n      [mq.phone]: 0,\n    },\n    display: {\n      default: null,\n      [mq.phone]: \"inline-flex\",\n    },\n    alignItems: {\n      default: null,\n      [mq.phone]: \"center\",\n    },\n    justifyContent: {\n      default: null,\n      [mq.phone]: \"center\",\n    },\n    minHeight: {\n      default: vlak.hit,\n      [mq.phone]: vlak.hit,\n    },\n    minWidth: vlak.hit,\n    paddingBlock: {\n      default: \"0.5rem\",\n      [mq.phone]: 0,\n    },\n    paddingInline: {\n      default: 0,\n      [mq.phone]: \"0.5rem\",\n    },\n    fontSize: {\n      default: \"0.875rem\",\n      [mq.phone]: vlak.controlFs,\n    },\n    fontFamily: \"inherit\",\n    fontWeight: 400,\n    color: {\n      default: vlak.gray,\n      [mq.forcedColors]: \"ButtonText\",\n    },\n    letterSpacing: \"-0.01em\",\n    textDecoration: \"none\",\n    textAlign: {\n      default: \"start\",\n      [mq.phone]: \"center\",\n    },\n    backgroundColor: \"transparent\",\n    backgroundImage: \"none\",\n    boxShadow: \"none\",\n    borderWidth: 0,\n    borderStyle: \"none\",\n    borderRadius: 0,\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  active: {\n    color: {\n      default: vlak.ink,\n      [mq.forcedColors]: \"Highlight\",\n    },\n    fontWeight: 600,\n    borderWidth: 0,\n    borderStyle: \"none\",\n    /* The underline is a box-shadow on paper; forced colors drop shadows, so it becomes a border there. */\n    boxShadow: \"inset 0 -1px 0\",\n    borderBottomWidth: {\n      default: 0,\n      [mq.forcedColors]: 2,\n    },\n    borderBottomStyle: {\n      default: \"none\",\n      [mq.forcedColors]: \"solid\",\n    },\n    borderBottomColor: {\n      default: \"transparent\",\n      [mq.forcedColors]: \"Highlight\",\n    },\n  },\n});\n\nexport interface TabsProps extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onChange\"> {\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n}\n\nexport const Tabs = React.forwardRef<HTMLDivElement, TabsProps>(function Tabs(\n  { value, defaultValue, onValueChange, children, ...props },\n  ref,\n) {\n  const idBase = React.useId();\n  const [panels, setPanels] = React.useState<ReadonlySet<string>>(() => new Set());\n  const registerPanel = React.useCallback((v: string) => {\n    setPanels((prev) => (prev.has(v) ? prev : new Set(prev).add(v)));\n    return () => setPanels((prev) => {\n      if (!prev.has(v)) return prev;\n      const next = new Set(prev);\n      next.delete(v);\n      return next;\n    });\n  }, []);\n  const [inner, setInner] = React.useState(defaultValue ?? \"\");\n  const [tabs, setTabs] = React.useState<ReadonlyMap<string, { element: HTMLButtonElement; disabled: boolean }>>(() => new Map());\n  const registerTab = React.useCallback((v: string, element: HTMLButtonElement, disabled: boolean) => {\n    setTabs((prev) => new Map(prev).set(v, { element, disabled }));\n    return () => setTabs((prev) => {\n      const next = new Map(prev);\n      next.delete(v);\n      return next;\n    });\n  }, []);\n  const isControlled = value !== undefined;\n  const requested = isControlled ? value : inner;\n  const enabled = [...tabs].filter(([, tab]) => !tab.disabled).sort(([, a], [, b]) =>\n    a.element.compareDocumentPosition(b.element) & 4 ? -1 : 1);\n  const current = enabled.some(([v]) => v === requested) ? requested : enabled[0]?.[0] ?? requested;\n  React.useEffect(() => {\n    if (!isControlled && enabled.length && current !== inner) setInner(current);\n  }, [isControlled, enabled.length, current, inner]);\n  const setValue = (next: string) => {\n    if (!isControlled) setInner(next);\n    onValueChange?.(next);\n  };\n  return (\n    <div ref={ref} {...props}>\n      <TabsContext.Provider value={{ value: current, setValue, idBase, panels, registerPanel, registerTab }}>\n        {children}\n      </TabsContext.Provider>\n    </div>\n  );\n});\n\nexport interface TabListProps extends React.HTMLAttributes<HTMLDivElement> {\n  /** Stacked tabs answer Up/Down instead of Left/Right. */\n  orientation?: \"horizontal\" | \"vertical\";\n}\n\n/** Roving tabs: arrows step, Home/End jump, and selection follows focus. */\nexport const TabList = React.forwardRef<HTMLDivElement, TabListProps>(function TabList(\n  { orientation = \"horizontal\", className, style, onKeyDown, ...props },\n  ref,\n) {\n  const vertical = orientation === \"vertical\";\n  const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {\n    onKeyDown?.(e);\n    if (e.defaultPrevented) return;\n    const prevKey = vertical ? \"ArrowUp\" : \"ArrowLeft\";\n    const nextKey = vertical ? \"ArrowDown\" : \"ArrowRight\";\n    if (e.key !== prevKey && e.key !== nextKey && e.key !== \"Home\" && e.key !== \"End\") return;\n    const tabs = Array.from(\n      e.currentTarget.querySelectorAll<HTMLButtonElement>('[role=\"tab\"]:not(:disabled)'),\n    );\n    const index = tabs.indexOf(document.activeElement as HTMLButtonElement);\n    if (index === -1 || tabs.length === 0) return;\n    e.preventDefault();\n    const next =\n      e.key === \"Home\"\n        ? 0\n        : e.key === \"End\"\n          ? tabs.length - 1\n          : e.key === nextKey\n            ? (index + 1) % tabs.length\n            : (index - 1 + tabs.length) % tabs.length;\n    tabs[next]?.focus();\n    tabs[next]?.click();\n  };\n  const sx = rs([\"rs-tabs\", vertical && \"rs-tabs-vertical\", className], styles.list, vertical && styles.vertical);\n  return (\n    <div\n      ref={ref}\n      role=\"tablist\"\n      aria-orientation={vertical ? \"vertical\" : undefined}\n      onKeyDown={handleKeyDown}\n      {...props}\n      className={sx.className}\n      style={{ ...sx.style, ...style }}\n    />\n  );\n});\n\nexport interface TabProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n  value: string;\n}\n\nexport const Tab = React.forwardRef<HTMLButtonElement, TabProps>(function Tab(\n  { value, className, style, onClick, disabled = false, ...props },\n  ref,\n) {\n  const ctx = useTabsContext(\"Tab\");\n  const element = React.useRef<HTMLButtonElement>(null);\n  const mergedRef = useMergedRefs(ref, element);\n  const registerTab = ctx.registerTab;\n  React.useEffect(() => element.current ? registerTab(value, element.current, disabled) : undefined, [registerTab, value, disabled]);\n  const selected = ctx.value === value;\n  const sx = rs([\"rs-tab\", selected && \"rs-tab-active\", className], styles.tab, selected && styles.active);\n  return (\n    <button\n      ref={mergedRef}\n      type=\"button\"\n      role=\"tab\"\n      disabled={disabled}\n      id={`${ctx.idBase}-tab-${value}`}\n      aria-selected={selected}\n      aria-controls={ctx.panels.has(value) ? `${ctx.idBase}-panel-${value}` : undefined}\n      tabIndex={selected ? 0 : -1}\n      onClick={(e) => {\n        onClick?.(e);\n        if (!e.defaultPrevented) ctx.setValue(value);\n      }}\n      {...props}\n      className={sx.className}\n      style={{ ...sx.style, ...style }}\n    />\n  );\n});\n\nexport interface TabPanelProps extends React.HTMLAttributes<HTMLDivElement> {\n  value: string;\n}\n\nexport const TabPanel = React.forwardRef<HTMLDivElement, TabPanelProps>(function TabPanel(\n  { value, ...props },\n  ref,\n) {\n  const ctx = useTabsContext(\"TabPanel\");\n  const selected = ctx.value === value;\n  const register = ctx.registerPanel;\n  React.useEffect(() => register(value), [register, value]);\n  return (\n    <div\n      ref={ref}\n      role=\"tabpanel\"\n      id={`${ctx.idBase}-panel-${value}`}\n      aria-labelledby={`${ctx.idBase}-tab-${value}`}\n      hidden={!selected}\n      {...props}\n    />\n  );\n});\n",
      "type": "registry:component",
      "target": "components/vlak/tabs.tsx"
    },
    {
      "path": "vlak/styles/tabs.css",
      "content": "/* ── tabs: generated from packages/react/src/components/tabs.tsx ── */\n.rs-tabs{display:flex;align-items:baseline;gap:1.375rem;max-width:22.5rem;border-width:0;border-style:none;border-bottom-width:0;border-bottom-style:none;border-bottom-color:transparent;box-shadow:none;background-color:transparent}\n@media (max-width: 640px){.rs-tabs{align-items:stretch;gap:0;width:100%;max-width:none;border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:var(--divider)}}\n.rs-tabs-vertical{flex-direction:column;align-items:stretch;gap:0;border-bottom-width:0;border-bottom-style:none}\n.rs-tab{appearance:none;box-sizing:border-box;min-height:var(--hit);min-width:var(--hit);padding-block:0.5rem;padding-inline:0;font-size:0.875rem;font-family:inherit;font-weight:400;color:var(--text-secondary);letter-spacing:-0.01em;text-decoration:none;text-align:start;background-color:transparent;background-image:none;box-shadow:none;border-width:0;border-style:none;border-radius:0;cursor:pointer}\n.rs-tab:focus-visible{outline-width:2px;outline-style:solid;outline-color:var(--text);outline-offset:2px}\n@media (max-width: 640px){.rs-tab{flex-grow:1;flex-shrink:1;flex-basis:0;display:inline-flex;align-items:center;justify-content:center;min-height:var(--hit);padding-block:0;padding-inline:0.5rem;font-size:var(--control-fs);text-align:center}}\n@media (forced-colors: active){.rs-tab{color:ButtonText}}\n.rs-tab-active{color:var(--text);font-weight:600;border-width:0;border-style:none;box-shadow:inset 0 -1px 0;border-bottom-width:0;border-bottom-style:none;border-bottom-color:transparent}\n@media (forced-colors: active){.rs-tab-active{color:Highlight;border-bottom-width:2px;border-bottom-style:solid;border-bottom-color:Highlight}}\n",
      "type": "registry:file",
      "target": "styles/vlak/tabs.css"
    }
  ],
  "meta": {
    "vlak": {
      "category": "navigation",
      "classes": [
        "rs-tabs",
        "rs-tabs-vertical",
        "rs-tab",
        "rs-tab-active"
      ],
      "snippet": "<div class=\"rs-tabs\"><span class=\"rs-tab rs-tab-active\">Overview</span><span class=\"rs-tab\">Activity</span><span class=\"rs-tab\">Settings</span></div>",
      "cssOnly": false,
      "registryDependencies": [
        "vlak-lib"
      ],
      "aliases": [
        "Tabs",
        "Tab list",
        "Tab bar"
      ],
      "example": "import { Tab, TabList, TabPanel, Tabs } from \"@noorddev/vlak-react\";\n\n<Tabs defaultValue=\"overview\">\n  <TabList aria-label=\"Project\">\n    <Tab value=\"overview\">Overview</Tab>\n    <Tab value=\"activity\">Activity</Tab>\n    <Tab value=\"settings\">Settings</Tab>\n  </TabList>\n  <TabPanel value=\"overview\">…</TabPanel>\n  <TabPanel value=\"activity\">…</TabPanel>\n  <TabPanel value=\"settings\">…</TabPanel>\n</Tabs>",
      "usage": {
        "use": [
          "Two to six views of the same object that share one place on the page.",
          "orientation=\"vertical\" on TabList for a stacked rail."
        ],
        "avoid": [
          "Navigation between pages; use NavigationMenu with real links.",
          "Sequential steps; use Stepper."
        ]
      },
      "keyboard": [
        {
          "keys": "Tab",
          "does": "Moves focus to the selected tab, then into the panel"
        },
        {
          "keys": "Arrow left, Arrow right",
          "does": "Moves to the previous or next tab and selects it (wraps)"
        },
        {
          "keys": "Arrow up, Arrow down",
          "does": "The same when orientation is vertical"
        },
        {
          "keys": "Home, End",
          "does": "First or last tab"
        }
      ],
      "a11y": [
        "TabList is role=\"tablist\" (aria-orientation when vertical); pass aria-label to name it.",
        "Tab is a <button role=\"tab\"> with aria-selected and aria-controls; TabPanel is role=\"tabpanel\" with aria-labelledby, hidden when not selected.",
        "One roving tab stop: only the selected tab is in the tab order.",
        "Without a default, the first enabled tab is selected. Removing or disabling the active tab reconciles to the first enabled tab; each target is at least 44px.",
        "Controlled with value and onValueChange, or uncontrolled with defaultValue."
      ]
    }
  }
}
