{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "menubar",
  "type": "registry:component",
  "title": "Menubar",
  "description": "Groups application menus in a compact 1px frame. Text-only triggers keep 44px targets and wrap to fit.",
  "registryDependencies": [
    "https://vlak.dev/r/vlak-base.json",
    "https://vlak.dev/r/inter.json",
    "https://vlak.dev/r/dropdown-menu.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/menubar.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\";\nimport { useOverlayPosition } from \"./use-overlay-position\";\nimport { MenuPanel, menuStyles, type DropdownMenuItem, type MenuCloseReason } from \"./dropdown-menu\";\n\nexport interface MenubarProps extends React.HTMLAttributes<HTMLDivElement> {\n  menus: Array<{ label: React.ReactNode; items: DropdownMenuItem[] }>;\n}\n\nconst styles = stylex.create({\n  bar: {\n    boxSizing: \"border-box\",\n    display: \"inline-flex\",\n    flexWrap: \"wrap\",\n    alignItems: \"center\",\n    gap: 2,\n    minWidth: 0,\n    maxWidth: \"100%\",\n    width: {\n      default: null,\n      [mq.phone]: \"100%\",\n    },\n    borderWidth: vlak.hairline,\n    borderStyle: \"solid\",\n    borderColor: vlak.divider,\n    borderRadius: {\n      default: vlak.radiusSm,\n      [mq.phone]: vlak.radiusSm,\n    },\n    padding: 2,\n  },\n  wrap: {\n    position: \"relative\",\n    display: \"flex\",\n    flex: \"0 1 auto\",\n    minWidth: 0,\n    maxWidth: \"100%\",\n  },\n  trigger: {\n    boxSizing: \"border-box\",\n    display: \"inline-flex\",\n    alignItems: \"center\",\n    justifyContent: \"center\",\n    minWidth: vlak.hit,\n    minHeight: vlak.hit,\n    maxWidth: \"100%\",\n    padding: \"0.5rem 0.75rem\",\n    borderWidth: 0,\n    borderRadius: vlak.radiusSm,\n    fontFamily: \"inherit\",\n    fontSize: vlak.controlFs,\n    lineHeight: 1.25,\n    color: vlak.ink,\n    backgroundColor: { default: \"transparent\", \":hover:not(:disabled):not([aria-disabled='true'])\": { default: null, [mq.hover]: { default: vlak.controlFill, [mq.forcedColors]: \"ButtonFace\" } } },\n    overflowWrap: \"anywhere\",\n    cursor: \"pointer\",\n    outlineWidth: { default: null, \":focus-visible\": 2 },\n    outlineStyle: { default: null, \":focus-visible\": \"solid\" },\n    outlineColor: vlak.ink,\n    outlineOffset: -2,\n  },\n  open: {\n    backgroundColor: { default: vlak.controlFill, \":hover:not(:disabled):not([aria-disabled='true'])\": { default: null, [mq.hover]: { default: vlak.controlFill, [mq.forcedColors]: \"Highlight\" } }, [mq.forcedColors]: \"Highlight\" },\n    color: { default: vlak.ink, [mq.forcedColors]: \"HighlightText\" },\n  },\n  panel: {\n    minWidth: \"10rem\",\n    width: \"max-content\",\n    maxWidth: \"calc(100vw - 16px)\",\n  },\n});\n\n/**\n * Dropdown menus in a hairline strip. The triggers are menuitems with one\n * roving tab stop; ArrowLeft/ArrowRight move between them and an open\n * menu follows.\n */\nexport const Menubar = React.forwardRef<HTMLDivElement, MenubarProps>(function Menubar(\n  { menus, className, style, onKeyDown, ...props },\n  ref,\n) {\n  const idBase = React.useId();\n  const rootRef = React.useRef<HTMLDivElement>(null);\n  const setRootRef = useMergedRefs(rootRef, ref);\n  const triggerRefs = React.useRef<Array<HTMLButtonElement | null>>([]);\n  const [focusIndex, setFocusIndex] = React.useState(0);\n  const [openIndex, setOpenIndex] = React.useState<number | null>(null);\n  const panelRef = React.useRef<HTMLDivElement>(null);\n  // Changing the open menu must also reposition the newly mounted panel.\n  const openTrigger = React.useMemo(() => ({\n    current: openIndex === null ? null : triggerRefs.current[openIndex] ?? null,\n  }), [openIndex]);\n  const placement = useOverlayPosition(openIndex !== null, panelRef, openTrigger);\n  const [initial, setInitial] = React.useState<\"first\" | \"last\">(\"first\");\n  const count = menus.length;\n\n  React.useEffect(() => {\n    if (openIndex === null) return;\n    const onPointerDown = (e: PointerEvent) => {\n      if (!rootRef.current?.contains(e.target as Node)) setOpenIndex(null);\n    };\n    document.addEventListener(\"pointerdown\", onPointerDown);\n    return () => document.removeEventListener(\"pointerdown\", onPointerDown);\n  }, [openIndex]);\n\n  const focusTrigger = (index: number) => {\n    setFocusIndex(index);\n    triggerRefs.current[index]?.focus();\n  };\n\n  const openMenu = (index: number, at: \"first\" | \"last\" = \"first\") => {\n    setFocusIndex(index);\n    setInitial(at);\n    setOpenIndex(index);\n  };\n\n  const closeMenu = (reason: MenuCloseReason) => {\n    setOpenIndex(null);\n    if (reason !== \"outside\") triggerRefs.current[focusIndex]?.focus();\n  };\n\n  const moveTo = (index: number) => {\n    const next = (index + count) % count;\n    if (openIndex !== null) openMenu(next);\n    else focusTrigger(next);\n  };\n\n  const onBarKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {\n    onKeyDown?.(e);\n    if (e.defaultPrevented || count === 0) return;\n    const index = triggerRefs.current.indexOf(e.target as HTMLButtonElement);\n    if (index < 0) return;\n    switch (e.key) {\n      case \"ArrowRight\":\n        e.preventDefault();\n        moveTo(index + 1);\n        return;\n      case \"ArrowLeft\":\n        e.preventDefault();\n        moveTo(index - 1);\n        return;\n      case \"Home\":\n        e.preventDefault();\n        moveTo(0);\n        return;\n      case \"End\":\n        e.preventDefault();\n        moveTo(count - 1);\n        return;\n      case \"ArrowDown\":\n      case \"Enter\":\n      case \" \":\n        e.preventDefault();\n        openMenu(index, \"first\");\n        return;\n      case \"ArrowUp\":\n        e.preventDefault();\n        openMenu(index, \"last\");\n        return;\n      case \"Escape\":\n        if (openIndex !== null) {\n          e.preventDefault();\n          closeMenu(\"escape\");\n        }\n        return;\n      default:\n        return;\n    }\n  };\n\n  const bar = rs([\"rs-menubar\", className], styles.bar);\n  const wrap = rs([\"rs-menubar-wrap\"], styles.wrap);\n  const menu = rs([\"rs-menu\"], menuStyles.menu, menuStyles.menuOverlay);\n  const panel = rs([\"rs-menubar-panel\"], styles.panel);\n  return (\n    <div\n      ref={setRootRef}\n      role=\"menubar\"\n      {...props}\n      className={bar.className}\n      style={{ ...bar.style, ...style }}\n      onKeyDown={onBarKeyDown}\n    >\n      {menus.map((entry, index) => {\n        const triggerId = `${idBase}-trigger-${index}`;\n        const menuId = `${idBase}-menu-${index}`;\n        const isOpen = openIndex === index;\n        const trigger = rs([\"rs-menubar-trigger\", isOpen && \"rs-menubar-trigger-open\"], styles.trigger, isOpen && styles.open);\n        return (\n          <div key={index} className={wrap.className} style={wrap.style}>\n            <button\n              ref={(el) => {\n                triggerRefs.current[index] = el;\n              }}\n              id={triggerId}\n              type=\"button\"\n              role=\"menuitem\"\n              className={trigger.className}\n              style={trigger.style}\n              tabIndex={focusIndex === index ? 0 : -1}\n              aria-haspopup=\"menu\"\n              aria-expanded={isOpen}\n              aria-controls={isOpen ? menuId : undefined}\n              onFocus={() => setFocusIndex(index)}\n              onClick={() => (isOpen ? closeMenu(\"select\") : openMenu(index))}\n              onPointerEnter={() => {\n                if (openIndex !== null && openIndex !== index) openMenu(index);\n              }}\n            >\n              {entry.label}\n            </button>\n            {isOpen && (\n              <MenuPanel\n                id={menuId}\n                items={entry.items}\n                labelledBy={triggerId}\n                initial={initial}\n                className={`${menu.className} ${panel.className}`}\n                panelRef={panelRef}\n                style={{ ...menu.style, ...panel.style, ...placement }}\n                onClose={closeMenu}\n                onHorizontal={(dir) => moveTo(index + dir)}\n              />\n            )}\n          </div>\n        );\n      })}\n    </div>\n  );\n});\n",
      "type": "registry:component",
      "target": "components/vlak/menubar.tsx"
    },
    {
      "path": "vlak/use-overlay-position.ts",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\n/** Keep lightweight overlays inside the visual viewport. The native top layer avoids clipping. */\nexport function useOverlayPosition(\n  open: boolean,\n  panel: React.RefObject<HTMLElement | null>,\n  anchor: React.RefObject<HTMLElement | null>,\n  point?: { x: number; y: number } | null,\n  placement: \"bottom\" | \"inline-end\" = \"bottom\",\n  options?: {\n    popover?: \"auto\" | \"manual\";\n    edge?: number;\n    matchAnchorWidth?: boolean;\n    /** Preferred block side. Collision handling may use the opposite side. */\n    side?: \"auto\" | \"top\" | \"bottom\";\n  },\n) {\n  const [position, setPosition] = React.useState<React.CSSProperties>({ visibility: \"hidden\" });\n  React.useLayoutEffect(() => {\n    const element = panel.current;\n    if (!open || !element) return;\n    const native = typeof element.showPopover === \"function\";\n    if (native) {\n      element.setAttribute(\"popover\", options?.popover ?? \"manual\");\n      if (!element.matches(\":popover-open\")) element.showPopover();\n    }\n    const place = () => {\n      const viewport = window.visualViewport;\n      const edge = Math.max(0, options?.edge ?? 8);\n      const leftEdge = (viewport?.offsetLeft ?? 0) + edge;\n      const topEdge = (viewport?.offsetTop ?? 0) + edge;\n      const viewportWidth = viewport?.width ?? window.innerWidth;\n      const viewportHeight = viewport?.height ?? window.innerHeight;\n      const rightEdge = leftEdge + viewportWidth - edge * 2;\n      const bottomEdge = topEdge + viewportHeight - edge * 2;\n      const target = anchor.current?.getBoundingClientRect();\n      const paint = getComputedStyle(element);\n      const borderX = (Number.parseFloat(paint.borderLeftWidth) || 0) + (Number.parseFloat(paint.borderRightWidth) || 0);\n      const borderY = (Number.parseFloat(paint.borderTopWidth) || 0) + (Number.parseFloat(paint.borderBottomWidth) || 0);\n      const matchAnchor = options?.matchAnchorWidth !== false;\n      let panelWidth = element.scrollWidth + borderX;\n      if (!matchAnchor) {\n        // Measure the authored width without the last placement’s viewport clamp.\n        // Restoring it here also keeps resize observation tied to the final layout.\n        const { width, minWidth, maxWidth } = element.style;\n        element.style.width = \"\";\n        element.style.minWidth = \"\";\n        element.style.maxWidth = \"\";\n        panelWidth = element.getBoundingClientRect().width;\n        Object.assign(element.style, { width, minWidth, maxWidth });\n      }\n      const width = Math.min(Math.max(panelWidth, matchAnchor ? target?.width ?? 0 : 0), viewportWidth - edge * 2);\n      const height = Math.min(element.scrollHeight + borderY, viewportHeight - edge * 2);\n      let left = point?.x ?? target?.left ?? leftEdge;\n      let top = point?.y ?? (target ? target.bottom + 6 : topEdge);\n      if (placement === \"inline-end\" && target) {\n        const rtl = getComputedStyle(anchor.current!).direction === \"rtl\";\n        left = rtl ? target.left - width - 4 : target.right + 4;\n        if (left + width > rightEdge) left = target.left - width - 4;\n        if (left < leftEdge) left = target.right + 4;\n        top = target.top;\n        if (top + height > bottomEdge) top = bottomEdge - height;\n      } else if (target && point?.y === undefined) {\n        const below = target.bottom + 6;\n        const above = target.top - height - 6;\n        if (options?.side === \"top\") {\n          top = above >= topEdge || below + height > bottomEdge ? above : below;\n        } else {\n          top = below + height <= bottomEdge || above < topEdge ? below : above;\n        }\n      } else if (top + height > bottomEdge) {\n        top = point?.y !== undefined ? point.y - height : bottomEdge - height;\n      }\n      left = Math.max(leftEdge, Math.min(left, rightEdge - width));\n      top = Math.max(topEdge, Math.min(top, bottomEdge - height));\n      const next: React.CSSProperties = {\n        position: \"fixed\", inset: \"auto\", left, top, margin: 0,\n        minWidth: matchAnchor ? Math.min(target?.width ?? 0, viewportWidth - edge * 2) : 0,\n        maxWidth: viewportWidth - edge * 2, maxHeight: viewportHeight - edge * 2,\n        width, overflow: \"auto\", visibility: \"visible\",\n      };\n      setPosition((previous) => JSON.stringify(previous) === JSON.stringify(next) ? previous : next);\n    };\n    place();\n    const observer = typeof ResizeObserver === \"undefined\" ? undefined : new ResizeObserver(place);\n    observer?.observe(element);\n    if (anchor.current) observer?.observe(anchor.current);\n    window.addEventListener(\"resize\", place);\n    window.addEventListener(\"scroll\", place, true);\n    window.visualViewport?.addEventListener(\"resize\", place);\n    window.visualViewport?.addEventListener(\"scroll\", place);\n    return () => {\n      observer?.disconnect();\n      window.removeEventListener(\"resize\", place);\n      window.removeEventListener(\"scroll\", place, true);\n      window.visualViewport?.removeEventListener(\"resize\", place);\n      window.visualViewport?.removeEventListener(\"scroll\", place);\n    };\n  }, [open, panel, anchor, point?.x, point?.y, placement, options?.popover, options?.edge, options?.matchAnchorWidth, options?.side]);\n  return position;\n}\n",
      "type": "registry:file",
      "target": "components/vlak/use-overlay-position.ts"
    },
    {
      "path": "vlak/styles/menubar.css",
      "content": "/* ── menubar: generated from packages/react/src/components/menubar.tsx ── */\n.rs-menubar{box-sizing:border-box;display:inline-flex;flex-wrap:wrap;align-items:center;gap:2px;min-width:0;max-width:100%;border-width:1px;border-style:solid;border-color:var(--divider);border-radius:var(--radius-sm);padding:2px}\n@media (max-width: 640px){.rs-menubar{width:100%;border-radius:var(--radius-sm)}}\n.rs-menubar-wrap{position:relative;display:flex;flex:0 1 auto;min-width:0;max-width:100%}\n.rs-menubar-trigger{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;min-width:var(--hit);min-height:var(--hit);max-width:100%;padding:0.5rem 0.75rem;border-width:0;border-radius:var(--radius-sm);font-family:inherit;font-size:var(--control-fs);line-height:1.25;color:var(--text);background-color:transparent;overflow-wrap:anywhere;cursor:pointer;outline-color:var(--text);outline-offset:-2px}\n.rs-menubar-trigger:focus-visible{outline-width:2px;outline-style:solid}\n@media (hover: hover) and (pointer: fine){.rs-menubar-trigger:hover:not(:disabled):not([aria-disabled='true']){background-color:var(--control-fill)}}\n@media (hover: hover) and (pointer: fine) and (forced-colors: active){.rs-menubar-trigger:hover:not(:disabled):not([aria-disabled='true']){background-color:ButtonFace}}\n.rs-menubar-trigger-open{background-color:var(--control-fill);color:var(--text)}\n@media (hover: hover) and (pointer: fine){.rs-menubar-trigger-open:hover:not(:disabled):not([aria-disabled='true']){background-color:var(--control-fill)}}\n@media (hover: hover) and (pointer: fine) and (forced-colors: active){.rs-menubar-trigger-open:hover:not(:disabled):not([aria-disabled='true']){background-color:Highlight}}\n@media (forced-colors: active){.rs-menubar-trigger-open{background-color:Highlight;color:HighlightText}}\n.rs-menubar-panel{min-width:10rem;width:max-content;max-width:calc(100vw - 16px)}\n",
      "type": "registry:file",
      "target": "styles/vlak/menubar.css"
    }
  ],
  "meta": {
    "vlak": {
      "category": "actions",
      "classes": [
        "rs-menubar",
        "rs-menubar-wrap",
        "rs-menubar-trigger",
        "rs-menubar-trigger-open",
        "rs-menubar-panel"
      ],
      "snippet": "<div class=\"rs-menubar\" role=\"menubar\" aria-label=\"Application menus\"><div class=\"rs-menubar-wrap\"><button type=\"button\" class=\"rs-menubar-trigger\" role=\"menuitem\" aria-haspopup=\"menu\" aria-expanded=\"false\" tabindex=\"0\">File</button></div><div class=\"rs-menubar-wrap\"><button type=\"button\" class=\"rs-menubar-trigger\" role=\"menuitem\" aria-haspopup=\"menu\" aria-expanded=\"false\" tabindex=\"-1\">Edit</button></div><div class=\"rs-menubar-wrap\"><button type=\"button\" class=\"rs-menubar-trigger\" role=\"menuitem\" aria-haspopup=\"menu\" aria-expanded=\"false\" tabindex=\"-1\">View</button></div></div>",
      "cssOnly": false,
      "registryDependencies": [
        "dropdown-menu",
        "vlak-lib"
      ],
      "aliases": [
        "Menubar",
        "Menu bar",
        "Application menu"
      ],
      "example": "import { Menubar } from \"@noorddev/vlak-react\";\n\n<Menubar\n  aria-label=\"Application menus\"\n  menus={[\n    { label: \"File\", items: [{ label: \"New\", onSelect: create }, { label: \"Open…\", onSelect: open }] },\n    { label: \"Edit\", items: [{ label: \"Undo\", onSelect: undo }, { label: \"Redo\", onSelect: redo }] },\n  ]}\n/>",
      "usage": {
        "use": [
          "Desktop-style application menus: File, Edit, View.",
          "Tools and editors with many commands grouped by verb."
        ],
        "avoid": [
          "Site navigation; use NavigationMenu.",
          "One menu; use DropdownMenu."
        ]
      },
      "keyboard": [
        {
          "keys": "Tab",
          "does": "Moves focus to the bar (one roving tab stop)"
        },
        {
          "keys": "Arrow left, Arrow right",
          "does": "Moves between menus; an open menu follows"
        },
        {
          "keys": "Home, End",
          "does": "First or last menu"
        },
        {
          "keys": "Arrow down, Enter, Space",
          "does": "Opens the menu on the first item"
        },
        {
          "keys": "Arrow up",
          "does": "Opens the menu on the last item"
        },
        {
          "keys": "Escape",
          "does": "Closes the open menu and returns focus to its trigger"
        }
      ],
      "a11y": [
        "Renders role=\"menubar\"; each trigger is a <button role=\"menuitem\"> with aria-haspopup=\"menu\", aria-expanded, and aria-controls.",
        "Open panels are role=\"menu\" labelled by their trigger, with the same keyboard model as DropdownMenu.",
        "Text-only triggers have 44px minimum hit targets and wrap inside the bar. The open trigger uses a full-surface fill, never a leading stripe.",
        "The markup snippet shows the closed appearance only. Use the React component for opening, placement, roving focus, and keyboard interaction."
      ]
    }
  }
}
