{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "crumb-bar",
  "type": "registry:component",
  "title": "Crumb bar",
  "description": "Keeps the current path visible while scrolling. Fixed at 72px with a 1px bottom rule when active.",
  "registryDependencies": [
    "https://vlak.dev/r/vlak-base.json",
    "https://vlak.dev/r/inter.json",
    "https://vlak.dev/r/breadcrumbs.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/crumb-bar.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 { inertAttrs } from \"./compat\";\n\nexport interface CrumbBarItem {\n  label: React.ReactNode;\n  href?: string;\n}\n\nexport interface CrumbBarProps extends React.HTMLAttributes<HTMLElement> {\n  trail: CrumbBarItem[];\n  /** Pixels of scroll before the bar solidifies and the crumbs fade in. */\n  threshold?: number;\n  /** Root crumb, held in the TOC column from 900px. */\n  root?: CrumbBarItem;\n  /** Abbreviated root shown on phones in place of the full label. */\n  rootShort?: React.ReactNode;\n}\n\nconst styles = stylex.create({\n  bar: {\n    position: \"fixed\",\n    top: 0,\n    insetInlineStart: 0,\n    insetInlineEnd: 0,\n    zIndex: vlak.zSticky,\n    display: \"flex\",\n    alignItems: \"center\",\n    height: {\n      default: \"4.5rem\",\n      [mq.phone]: \"4rem\",\n    },\n    fontSize: {\n      default: \"0.8125rem\",\n      [mq.phone]: \"0.75rem\",\n    },\n    fontWeight: 500,\n    letterSpacing: \"-0.01em\",\n    lineHeight: 1.3,\n    backgroundColor: \"transparent\",\n    borderBottomWidth: vlak.hairline,\n    borderBottomStyle: \"solid\",\n    borderBottomColor: \"transparent\",\n    transition: vlak.transition,\n  },\n  scrolled: {\n    backgroundColor: vlak.paper,\n    borderBottomColor: vlak.divider,\n  },\n  inner: {\n    display: \"flex\",\n    alignItems: \"center\",\n    gap: {\n      default: \"1.25rem\",\n      [mq.at899]: \"0.375rem\",\n    },\n    width: \"100%\",\n    minWidth: 0,\n    paddingTop: 0,\n    paddingBottom: 0,\n    paddingInlineEnd: {\n      default: \"3.5rem\",\n      [mq.phone]: \"3.125rem\",\n      [mq.mobileGrid]: \"3.5rem\",\n    },\n    paddingInlineStart: {\n      default: \"4.75rem\",\n      [mq.rail]: vlak.pad,\n      [mq.phone]: \"3.875rem\",\n      [mq.mobileGrid]: \"4.625rem\",\n    },\n    marginInlineStart: {\n      default: null,\n      [mq.rail]: 204,\n    },\n  },\n  crumbs: {\n    listStyle: \"none\",\n    padding: 0,\n    margin: 0,\n    minWidth: 0,\n    overflow: \"hidden\",\n    whiteSpace: \"nowrap\",\n    opacity: 0,\n    visibility: \"hidden\",\n    pointerEvents: \"none\",\n    transition: {\n      default: `opacity ${vlak.duration} ${vlak.ease}, visibility 0s ${vlak.ease} ${vlak.duration}`,\n      [mq.reduce]: \"none\",\n    },\n    fontSize: {\n      default: \"0.8125rem\",\n      [mq.phone]: vlak.controlFs,\n    },\n    color: vlak.ink,\n    letterSpacing: \"-0.01em\",\n    display: \"flex\",\n    alignItems: {\n      default: \"baseline\",\n      [mq.phone]: \"center\",\n    },\n    flexWrap: {\n      default: null,\n      [mq.phone]: \"wrap\",\n    },\n    gap: {\n      default: \"0.5rem\",\n      [mq.phone]: \"0.375rem\",\n    },\n  },\n  crumbsOn: {\n    opacity: 1,\n    visibility: \"visible\",\n    pointerEvents: \"auto\",\n    transition: {\n      default: `opacity ${vlak.duration} ${vlak.ease}, visibility 0s`,\n      [mq.reduce]: \"none\",\n    },\n  },\n  item: {\n    display: \"inline-flex\",\n    alignItems: {\n      default: \"baseline\",\n      [mq.phone]: \"center\",\n    },\n    gap: {\n      default: \"0.5rem\",\n      [mq.phone]: \"0.375rem\",\n    },\n  },\n  root: {\n    display: \"inline-flex\",\n    alignItems: \"center\",\n    minHeight: vlak.hit,\n    minWidth: vlak.hit,\n    flexShrink: 0,\n    color: {\n      default: vlak.ink,\n      \":link\": vlak.ink,\n      \":visited\": vlak.ink,\n    },\n    textDecoration: \"none\",\n    transition: `color ${vlak.durationSnap} ${vlak.ease}`,\n    width: {\n      default: null,\n      [mq.at900]: 184,\n    },\n    fontWeight: {\n      default: 500,\n      [mq.at899]: 600,\n    },\n  },\n  rootFull: {\n    display: {\n      default: null,\n      [mq.phone]: \"none\",\n    },\n  },\n  rootShort: {\n    display: {\n      default: \"none\",\n      [mq.phone]: \"inline\",\n    },\n  },\n  link: {\n    minWidth: vlak.hit,\n    transition: {\n      default: vlak.transition,\n      [mq.reduce]: \"none\",\n    },\n    color: {\n      default: vlak.gray,\n      \":link\": vlak.gray,\n      \":visited\": vlak.gray,\n      \":hover\": { default: null, [mq.hover]: vlak.ink },\n      \":active\": vlak.ink,\n      \":focus-visible\": vlak.ink,\n    },\n    fontWeight: 400,\n    textDecoration: {\n      default: \"none\",\n      \":hover\": { default: null, [mq.hover]: \"underline\" },\n    },\n    textUnderlineOffset: 3,\n    backgroundColor: \"transparent\",\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    display: {\n      default: \"inline-flex\",\n      [mq.phone]: \"inline-flex\",\n    },\n    alignItems: {\n      default: \"center\",\n      [mq.phone]: \"center\",\n    },\n    minHeight: {\n      default: vlak.hit,\n      [mq.phone]: vlak.hit,\n    },\n  },\n  sep: {\n    color: vlak.gray,\n  },\n  here: {\n    color: vlak.ink,\n    fontWeight: 500,\n    overflow: \"hidden\",\n    textOverflow: \"ellipsis\",\n    display: {\n      default: null,\n      [mq.phone]: \"inline-flex\",\n    },\n    alignItems: {\n      default: null,\n      [mq.phone]: \"center\",\n    },\n    minHeight: {\n      default: null,\n      [mq.phone]: vlak.hit,\n    },\n  },\n});\n\n/**\n * The fixed top bar of the house chrome. Transparent at rest; once the\n * page cover scrolls away it gains the paper background and its bottom\n * hairline, and the breadcrumbs fade in. While hidden the trail is\n * inert and invisible, so nothing focuses into an unseen link.\n */\nexport const CrumbBar = React.forwardRef<HTMLElement, CrumbBarProps>(function CrumbBar(\n  { trail, threshold = 110, root, rootShort, className, style, ...props },\n  ref,\n) {\n  const [scrolled, setScrolled] = React.useState(false);\n\n  React.useEffect(() => {\n    const onScroll = () => setScrolled(window.scrollY > threshold);\n    onScroll();\n    window.addEventListener(\"scroll\", onScroll, { passive: true });\n    return () => window.removeEventListener(\"scroll\", onScroll);\n  }, [threshold]);\n\n  const bar = rs([\"rs-crumb-bar\", scrolled && \"rs-crumb-bar-scrolled\", className], styles.bar, scrolled && styles.scrolled);\n  const inner = rs([\"rs-crumb-bar-inner\"], styles.inner);\n  const crumbs = rs([\"rs-crumbs\", \"rs-crumb-crumbs\", scrolled && \"rs-crumb-crumbs-on\"], styles.crumbs, scrolled && styles.crumbsOn);\n  const item = rs([\"rs-crumb-item\"], styles.item);\n  const rootSx = rs([\"rs-crumb-root\"], styles.root);\n  const rootFull = rs([\"rs-crumb-root-full\"], styles.rootFull);\n  const rootShortSx = rs([\"rs-crumb-root-short\"], styles.rootShort);\n  const link = rs([\"rs-crumbs-link\", \"rs-crumb-link\"], styles.link);\n  const sep = rs([\"rs-crumbs-sep\", \"rs-crumb-sep\"], styles.sep);\n  const here = rs([\"rs-crumbs-here\", \"rs-crumb-here\"], styles.here);\n\n  const rootLabel = rootShort ? (\n    <>\n      <span className={rootFull.className} style={rootFull.style}>\n        {root?.label}\n      </span>\n      <span className={rootShortSx.className} style={rootShortSx.style}>\n        {rootShort}\n      </span>\n    </>\n  ) : (\n    root?.label\n  );\n\n  return (\n    <nav ref={ref} aria-label=\"Breadcrumb\" {...props} className={bar.className} style={{ ...bar.style, ...style }}>\n      <div className={inner.className} style={inner.style}>\n        {root &&\n          (root.href ? (\n            <a className={rootSx.className} style={rootSx.style} href={root.href}>\n              {rootLabel}\n            </a>\n          ) : (\n            <span className={rootSx.className} style={rootSx.style}>\n              {rootLabel}\n            </span>\n          ))}\n        <ol className={crumbs.className} style={crumbs.style} {...inertAttrs(!scrolled)}>\n          {trail.map((crumb, index) => {\n            const last = index === trail.length - 1;\n            return (\n              <li key={index} className={item.className} style={item.style}>\n                {index > 0 && (\n                  <span className={sep.className} style={sep.style} aria-hidden=\"true\">\n                    /\n                  </span>\n                )}\n                {last ? (\n                  <span className={here.className} style={here.style} aria-current=\"page\">\n                    {crumb.label}\n                  </span>\n                ) : crumb.href ? (\n                  <a className={link.className} style={link.style} href={crumb.href}>\n                    {crumb.label}\n                  </a>\n                ) : (\n                  <span className={link.className} style={link.style}>\n                    {crumb.label}\n                  </span>\n                )}\n              </li>\n            );\n          })}\n        </ol>\n      </div>\n    </nav>\n  );\n});\n",
      "type": "registry:component",
      "target": "components/vlak/crumb-bar.tsx"
    },
    {
      "path": "vlak/compat.ts",
      "content": "import * as React from \"react\";\n\n/* React 18 and 19 spell newer DOM attributes differently: 19 knows\n   popoverTarget and boolean inert, 18 only passes lowercase unknown\n   attributes through as strings. These helpers emit the right shape. */\nconst major = Number.parseInt(React.version.split(\".\")[0] ?? \"19\", 10);\n\n/** Attributes that point a button at a popover element. */\nexport function popoverTargetAttrs(id: string): Record<string, string> {\n  return major >= 19 ? { popoverTarget: id } : { popovertarget: id };\n}\n\n/** The inert attribute, on when `on` is true. */\nexport function inertAttrs(on: boolean): Record<string, boolean | string | undefined> {\n  if (major >= 19) return { inert: on };\n  return { inert: on ? \"\" : undefined };\n}\n",
      "type": "registry:file",
      "target": "components/vlak/compat.ts"
    },
    {
      "path": "vlak/styles/crumb-bar.css",
      "content": "/* ── crumb-bar: generated from packages/react/src/components/crumb-bar.tsx ── */\n.rs-crumb-bar{position:fixed;top:0;inset-inline-start:0;inset-inline-end:0;z-index:var(--z-sticky);display:flex;align-items:center;height:4.5rem;font-size:0.8125rem;font-weight:500;letter-spacing:-0.01em;line-height:1.3;background-color:transparent;border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:transparent;transition:var(--transition)}\n@media (max-width: 640px){.rs-crumb-bar{height:4rem;font-size:0.75rem}}\n.rs-crumb-bar-scrolled{background-color:var(--bg);border-bottom-color:var(--divider)}\n.rs-crumb-bar-inner{display:flex;align-items:center;gap:1.25rem;width:100%;min-width:0;padding-top:0;padding-bottom:0;padding-inline-end:3.5rem;padding-inline-start:4.75rem}\n@media (max-width: 899px){.rs-crumb-bar-inner{gap:0.375rem}}\n@media (max-width: 640px){.rs-crumb-bar-inner{padding-inline-end:3.125rem;padding-inline-start:3.875rem}}\n@media (max-width: 480px){.rs-crumb-bar-inner{padding-inline-end:3.5rem;padding-inline-start:4.625rem}}\n@media (min-width: 1024px){.rs-crumb-bar-inner{padding-inline-start:var(--pad);margin-inline-start:204px}}\n.rs-crumbs.rs-crumb-crumbs{list-style:none;padding:0;margin:0;min-width:0;overflow:hidden;white-space:nowrap;opacity:0;visibility:hidden;pointer-events:none;transition:opacity var(--duration) var(--ease), visibility 0s var(--ease) var(--duration);font-size:0.8125rem;color:var(--text);letter-spacing:-0.01em;display:flex;align-items:baseline;gap:0.5rem}\n@media (prefers-reduced-motion: reduce){.rs-crumbs.rs-crumb-crumbs{transition:none}}\n@media (max-width: 640px){.rs-crumbs.rs-crumb-crumbs{font-size:var(--control-fs);align-items:center;flex-wrap:wrap;gap:0.375rem}}\n.rs-crumb-crumbs-on{opacity:1;visibility:visible;pointer-events:auto;transition:opacity var(--duration) var(--ease), visibility 0s}\n@media (prefers-reduced-motion: reduce){.rs-crumb-crumbs-on{transition:none}}\n.rs-crumb-item{display:inline-flex;align-items:baseline;gap:0.5rem}\n@media (max-width: 640px){.rs-crumb-item{align-items:center;gap:0.375rem}}\n.rs-crumb-root{display:inline-flex;align-items:center;min-height:var(--hit);min-width:var(--hit);flex-shrink:0;color:var(--text);text-decoration:none;transition:color var(--duration-snap) var(--ease);font-weight:500}\n.rs-crumb-root:link{color:var(--text)}\n.rs-crumb-root:visited{color:var(--text)}\n@media (min-width: 900px){.rs-crumb-root{width:184px}}\n@media (max-width: 899px){.rs-crumb-root{font-weight:600}}\n@media (max-width: 640px){.rs-crumb-root-full{display:none}}\n.rs-crumb-root-short{display:none}\n@media (max-width: 640px){.rs-crumb-root-short{display:inline}}\n.rs-crumbs-link.rs-crumb-link{min-width:var(--hit);transition:var(--transition);color:var(--text-secondary);font-weight:400;text-decoration:none;text-underline-offset:3px;background-color:transparent;display:inline-flex;align-items:center;min-height:var(--hit)}\n.rs-crumbs-link.rs-crumb-link:link{color:var(--text-secondary)}\n.rs-crumbs-link.rs-crumb-link:visited{color:var(--text-secondary)}\n.rs-crumbs-link.rs-crumb-link:active{color:var(--text)}\n.rs-crumbs-link.rs-crumb-link:focus-visible{color:var(--text);outline-width:2px;outline-style:solid;outline-color:var(--text);outline-offset:2px}\n@media (prefers-reduced-motion: reduce){.rs-crumbs-link.rs-crumb-link{transition:none}}\n@media (hover: hover) and (pointer: fine){.rs-crumbs-link.rs-crumb-link:hover{color:var(--text);text-decoration:underline}}\n@media (max-width: 640px){.rs-crumbs-link.rs-crumb-link{display:inline-flex;align-items:center;min-height:var(--hit)}}\n.rs-crumbs-sep.rs-crumb-sep{color:var(--text-secondary)}\n.rs-crumbs-here.rs-crumb-here{color:var(--text);font-weight:500;overflow:hidden;text-overflow:ellipsis}\n@media (max-width: 640px){.rs-crumbs-here.rs-crumb-here{display:inline-flex;align-items:center;min-height:var(--hit)}}\n",
      "type": "registry:file",
      "target": "styles/vlak/crumb-bar.css"
    }
  ],
  "meta": {
    "vlak": {
      "category": "navigation",
      "classes": [
        "rs-crumb-bar",
        "rs-crumb-bar-scrolled",
        "rs-crumb-bar-inner",
        "rs-crumb-root",
        "rs-crumb-root-full",
        "rs-crumb-root-short",
        "rs-crumb-crumbs",
        "rs-crumb-crumbs-on",
        "rs-crumb-item",
        "rs-crumb-here",
        "rs-crumb-link",
        "rs-crumb-sep"
      ],
      "snippet": "<nav class=\"rs-crumb-bar rs-crumb-bar-scrolled\"><div class=\"rs-crumb-bar-inner\"><a class=\"rs-crumb-root\" href=\"/\"><span class=\"rs-crumb-root-full\">Vlak</span><span class=\"rs-crumb-root-short\">rs</span></a><p class=\"rs-crumbs\"><span>Docs</span><span class=\"rs-crumbs-sep\">/</span><span class=\"rs-crumbs-here\">Components</span></p></div></nav>",
      "cssOnly": false,
      "registryDependencies": [
        "breadcrumbs",
        "vlak-lib"
      ],
      "aliases": [
        "Crumb bar",
        "Sticky header",
        "Top bar",
        "App bar"
      ],
      "example": "import { CrumbBar } from \"@noorddev/vlak-react\";\n\n<CrumbBar\n  root={{ label: \"Renato Valdés Olmos\", href: \"/\" }}\n  rootShort=\"RVO\"\n  trail={[{ label: \"Components\", href: \"/components\" }, { label: \"Switch\" }]}\n  threshold={110}\n/>",
      "usage": {
        "use": [
          "The fixed top bar of a long page: transparent over the cover, paper and a hairline once scrolled.",
          "A root mark plus the trail of the current page."
        ],
        "avoid": [
          "Pages without a cover; use Breadcrumbs in the flow.",
          "Bars that hold actions or search; this one holds the trail only."
        ]
      },
      "keyboard": [
        {
          "keys": "Tab",
          "does": "Moves to the root link, then the trail links once the bar is scrolled in"
        },
        {
          "keys": "Enter",
          "does": "Follows the link"
        }
      ],
      "a11y": [
        "Renders <nav aria-label=\"Breadcrumb\">; the trail is an ordered list with aria-current=\"page\" on the last item.",
        "While the bar is transparent the trail is inert, so nothing focuses into an invisible link."
      ]
    }
  }
}
