{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "shell-app-menu",
  "type": "registry:component",
  "title": "Application menu",
  "description": "The in-window menu bar, with keyboard traversal",
  "author": "sudo-kw <https://github.com/sudo-kw>",
  "dependencies": [
    "@gpuix/native@0.11.0",
    "@gpuix/react@0.11.0",
    "effect@4.1.2",
    "react@^19.2.4"
  ],
  "registryDependencies": [
    "https://gpuixui.sudokw.com/gpuix-keyboard.json",
    "https://gpuixui.sudokw.com/gpuix-primitives.json",
    "https://gpuixui.sudokw.com/gpuix-tokens.json",
    "https://gpuixui.sudokw.com/gpuix-tw.json"
  ],
  "files": [
    {
      "path": "packages/shell/src/app-menu/app-menu.tsx",
      "target": "@ui/gpuixui/shell/app-menu/app-menu.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport type { KeyEvent, Props, PublicInstance, StyleDesc } from \"@gpuix/react\";\nimport { useGpuix } from \"@gpuix/react\";\nimport {\n  FloatingLayer,\n  floatingRootStyle,\n  mergeStyles,\n} from \"@gpuix/react/floating\";\nimport { formatSymbols, type Platform } from \"../../keyboard/index.js\";\nimport { isPlainKey } from \"../../primitives/index.js\";\nimport {\n  cn,\n  KEYBOARD_SHORTCUT_KEYS,\n  KEYCAP,\n  KEYCAP_TEXT,\n  MENU_CONTENT,\n  MENU_ITEM,\n} from \"../../tw/index.js\";\nimport * as Arr from \"effect/Array\";\nimport * as Option from \"effect/Option\";\nimport { Fragment, useLayoutEffect, useRef, useState } from \"react\";\nimport { recipeStyle } from \"../recipe.js\";\nimport { currentPlatform, disabledOpacity, useTheme } from \"../theme.js\";\nimport type { Theme } from \"../theme.js\";\nimport {\n  dismiss,\n  hoverOpen,\n  initial,\n  step,\n  type AppMenuSpec,\n  type MenuBarState,\n} from \"./logic.js\";\n\nconst MENU_BAR = \"flex flex-row items-center\";\nconst MENU_TRIGGER =\n  \"flex flex-row items-center rounded-md px-2 py-1 text-sm font-medium font-sans text-foreground\";\nconst MENU_ITEM_LABEL = \"text-sm font-medium font-sans\";\n\nexport interface AppMenuItem {\n  readonly id: string;\n  readonly label: string;\n\n  readonly accelerator?: string;\n  readonly disabled?: boolean;\n}\n\nexport interface AppMenuEntry {\n  readonly id: string;\n  readonly label: string;\n  readonly disabled?: boolean;\n  readonly items: ReadonlyArray<AppMenuItem>;\n}\n\nexport interface AppMenuProps extends Omit<Props, \"children\"> {\n  readonly label?: string;\n  readonly menus: ReadonlyArray<AppMenuEntry>;\n\n  readonly platform?: Platform;\n\n  readonly onSelect?: (menuId: string, itemId: string) => void;\n  readonly className?: string;\n}\n\nconst triggerStyle = (\n  theme: Theme,\n  open: boolean,\n  highlighted: boolean,\n  disabled: boolean,\n): StyleDesc => ({\n  ...recipeStyle(MENU_TRIGGER, theme),\n  ...(disabled\n    ? { opacity: disabledOpacity }\n    : open\n      ? { backgroundColor: theme.colors.active }\n      : highlighted\n        ? { backgroundColor: theme.colors.hover }\n        : { hover: { backgroundColor: theme.colors.hover } }),\n});\n\nconst itemStyle = (\n  theme: Theme,\n  active: boolean,\n  disabled: boolean,\n): StyleDesc => ({\n  ...recipeStyle(cn(MENU_ITEM, \"justify-between\"), theme),\n  ...(disabled\n    ? { opacity: disabledOpacity }\n    : active\n      ? { backgroundColor: theme.colors.hover }\n      : { hover: { backgroundColor: theme.colors.hover } }),\n  cursor: disabled ? \"not-allowed\" : \"pointer\",\n});\n\nexport const AppMenu = ({\n  label,\n  menus,\n  platform,\n  onSelect,\n  className,\n  style,\n  ...rest\n}: AppMenuProps) => {\n  const theme = useTheme();\n  const renderer = useGpuix().renderer;\n  const [state, setState] = useState<MenuBarState>(initial);\n\n  const triggerRefs = useRef<Array<PublicInstance | null>>([]);\n  const surfaceRef = useRef<PublicInstance | null>(null);\n\n  const specs: ReadonlyArray<AppMenuSpec> = Arr.map(menus, (menu) => ({\n    id: menu.id,\n    disabled: menu.disabled === true,\n    items: Arr.map(menu.items, (item) => ({\n      id: item.id,\n      disabled: item.disabled === true,\n    })),\n  }));\n\n  useLayoutEffect(() => {\n    if (Option.isSome(state.openMenu) && surfaceRef.current !== null) {\n      renderer?.focusElement?.(surfaceRef.current.id);\n    }\n  }, [state.openMenu, renderer]);\n\n  const firstEnabled = Arr.findFirstIndex(\n    menus,\n    (menu) => menu.disabled !== true,\n  );\n\n  const stopIndex = Option.getOrElse(\n    Option.orElse(state.activeMenu, () => firstEnabled),\n    () => 0,\n  );\n\n  const openHighlighted = (): void => {\n    Option.match(\n      Option.orElse(state.activeMenu, () => firstEnabled),\n      {\n        onNone: () => undefined,\n        onSome: (menu) => setState(step(state, specs, { _tag: \"Open\", menu })),\n      },\n    );\n  };\n\n  const runActive = (): void => {\n    Option.match(state.openMenu, {\n      onNone: () => undefined,\n      onSome: (menu) =>\n        Option.match(Arr.get(menus, menu), {\n          onNone: () => undefined,\n          onSome: (entry) =>\n            Option.match(\n              Option.flatMap(state.activeItem, (i) => Arr.get(entry.items, i)),\n              {\n                onNone: () => undefined,\n                onSome: (item) => {\n                  if (item.disabled === true) return;\n                  onSelect?.(entry.id, item.id);\n                  setState(dismiss(state).state);\n                },\n              },\n            ),\n        }),\n    });\n  };\n\n  const handleKeyDown = (event: KeyEvent): void => {\n    const key = event.key ?? \"\";\n    if (!isPlainKey(event.modifiers)) return;\n\n    if (key === \"escape\") {\n      const result = dismiss(state);\n      setState(result.state);\n      Option.match(result.refocus, {\n        onNone: () => undefined,\n        onSome: (menu) => {\n          const trigger = triggerRefs.current[menu];\n          if (trigger !== null && trigger !== undefined) {\n            renderer?.focusElement?.(trigger.id);\n          }\n        },\n      });\n      return;\n    }\n\n    if (key === \"tab\") {\n      setState(initial);\n      return;\n    }\n\n    if (key === \"left\" || key === \"right\") {\n      setState(\n        step(state, specs, {\n          _tag: \"MoveBar\",\n          delta: key === \"right\" ? 1 : -1,\n        }),\n      );\n      return;\n    }\n\n    if (key === \"down\") {\n      if (Option.isSome(state.openMenu)) {\n        setState(step(state, specs, { _tag: \"MoveItem\", delta: 1 }));\n      } else {\n        openHighlighted();\n      }\n      return;\n    }\n\n    if (key === \"up\" && Option.isSome(state.openMenu)) {\n      setState(step(state, specs, { _tag: \"MoveItem\", delta: -1 }));\n      return;\n    }\n\n    if (key === \"enter\" || key === \"space\") {\n      if (Option.isSome(state.openMenu)) runActive();\n      else openHighlighted();\n    }\n  };\n\n  return (\n    <div\n      role=\"menubar\"\n      {...(label === undefined ? {} : { \"aria-label\": label })}\n      {...rest}\n      onKeyDown={handleKeyDown}\n      style={mergeStyles(\n        mergeStyles(\n          recipeStyle(cn(MENU_BAR, className), theme),\n          floatingRootStyle(),\n        ),\n        style,\n      )}\n    >\n      {Arr.map(menus, (menu, index) => {\n        const open = Option.contains(state.openMenu, index);\n        const highlighted = open || Option.contains(state.activeMenu, index);\n        const disabled = menu.disabled === true;\n        return (\n          <Fragment key={menu.id}>\n            <div\n              role=\"menuitem\"\n              tabIndex={index === stopIndex ? 0 : -1}\n              aria-haspopup=\"menu\"\n              aria-expanded={open}\n              aria-disabled={disabled}\n              testId={`app-menu-trigger-${menu.id}`}\n              ref={(instance) => {\n                triggerRefs.current[index] = instance;\n              }}\n              onClick={() => {\n                if (disabled) return;\n                setState(\n                  Option.contains(state.openMenu, index)\n                    ? dismiss(state).state\n                    : step(state, specs, { _tag: \"Open\", menu: index }),\n                );\n              }}\n              onMouseEnter={() => setState(hoverOpen(state, specs, index))}\n              style={triggerStyle(theme, open, highlighted, disabled)}\n            >\n              {menu.label}\n            </div>\n            {open ? (\n              <FloatingLayer\n                role=\"menu\"\n                aria-label={menu.label}\n                tabIndex={-1}\n                testId={`app-menu-panel-${menu.id}`}\n                side=\"bottom\"\n                align=\"start\"\n                sideOffset={2}\n                ref={(instance) => {\n                  surfaceRef.current = instance;\n                }}\n                style={recipeStyle(cn(MENU_CONTENT, \"min-w-40\"), theme)}\n              >\n                {Arr.map(menu.items, (item, itemIndex) => {\n                  const active = Option.contains(state.activeItem, itemIndex);\n                  const itemDisabled = item.disabled === true;\n                  return (\n                    <div\n                      key={item.id}\n                      role=\"menuitem\"\n                      aria-selected={active}\n                      aria-disabled={itemDisabled}\n                      testId={`app-menu-item-${item.id}`}\n                      onClick={() => {\n                        if (itemDisabled) return;\n                        onSelect?.(menu.id, item.id);\n                        setState(dismiss(state).state);\n                      }}\n                      style={itemStyle(theme, active, itemDisabled)}\n                    >\n                      <text\n                        style={{\n                          ...recipeStyle(MENU_ITEM_LABEL, theme),\n                          color: itemDisabled\n                            ? theme.colors.disabledForeground\n                            : theme.colors.popoverForeground,\n                        }}\n                      >\n                        {item.label}\n                      </text>\n                      {item.accelerator === undefined ? null : (\n                        <div style={recipeStyle(KEYBOARD_SHORTCUT_KEYS, theme)}>\n                          {Arr.map(\n                            formatSymbols(\n                              item.accelerator,\n                              platform ?? currentPlatform,\n                            ),\n                            (glyph, keyIndex) => (\n                              <div\n                                key={keyIndex}\n                                style={recipeStyle(KEYCAP, theme)}\n                              >\n                                <text style={recipeStyle(KEYCAP_TEXT, theme)}>\n                                  {glyph}\n                                </text>\n                              </div>\n                            ),\n                          )}\n                        </div>\n                      )}\n                    </div>\n                  );\n                })}\n              </FloatingLayer>\n            ) : null}\n          </Fragment>\n        );\n      })}\n    </div>\n  );\n};\n"
    },
    {
      "path": "packages/shell/src/app-menu/logic.ts",
      "target": "@ui/gpuixui/shell/app-menu/logic.ts",
      "type": "registry:lib",
      "content": "import * as Arr from \"effect/Array\";\nimport * as Option from \"effect/Option\";\nimport { Match } from \"effect\";\nimport { decideMove } from \"../../primitives/index.js\";\n\nexport interface AppMenuItemSpec {\n  readonly id: string;\n  readonly disabled: boolean;\n}\n\nexport interface AppMenuSpec {\n  readonly id: string;\n  readonly disabled: boolean;\n  readonly items: ReadonlyArray<AppMenuItemSpec>;\n}\n\nexport interface MenuBarState {\n  readonly activeMenu: Option.Option<number>;\n\n  readonly openMenu: Option.Option<number>;\n\n  readonly activeItem: Option.Option<number>;\n}\n\nexport const initial: MenuBarState = {\n  activeMenu: Option.none(),\n  openMenu: Option.none(),\n  activeItem: Option.none(),\n};\n\nexport type MenuBarAction =\n  | { readonly _tag: \"MoveBar\"; readonly delta: 1 | -1 }\n  | { readonly _tag: \"Open\"; readonly menu: number }\n  | { readonly _tag: \"MoveItem\"; readonly delta: 1 | -1 }\n  | { readonly _tag: \"Dismiss\" };\n\nconst arrowKey = (\n  delta: 1 | -1,\n  orientation: \"horizontal\" | \"vertical\",\n): string =>\n  orientation === \"horizontal\"\n    ? delta === 1\n      ? \"right\"\n      : \"left\"\n    : delta === 1\n      ? \"down\"\n      : \"up\";\n\nconst moveIn = (\n  current: Option.Option<number>,\n  count: number,\n  delta: 1 | -1,\n  orientation: \"horizontal\" | \"vertical\",\n  isDisabled: (index: number) => boolean,\n): Option.Option<number> =>\n  Option.match(current, {\n    onNone: () => {\n      const enabled = Arr.filter(\n        Arr.range(0, count - 1),\n        (i) => !isDisabled(i),\n      );\n      return Arr.get(enabled, delta === 1 ? 0 : Arr.length(enabled) - 1);\n    },\n    onSome: (index) =>\n      decideMove(\n        index,\n        count,\n        arrowKey(delta, orientation),\n        orientation,\n        isDisabled,\n      ),\n  });\n\nconst menuIsDisabled = (\n  menus: ReadonlyArray<AppMenuSpec>,\n  index: number,\n): boolean => Option.exists(Arr.get(menus, index), (menu) => menu.disabled);\n\nconst itemsOf = (\n  menus: ReadonlyArray<AppMenuSpec>,\n  menu: number,\n): ReadonlyArray<AppMenuItemSpec> =>\n  Option.match(Arr.get(menus, menu), {\n    onNone: () => [],\n    onSome: (entry) => entry.items,\n  });\n\nconst itemIsDisabled = (\n  items: ReadonlyArray<AppMenuItemSpec>,\n  index: number,\n): boolean => Option.exists(Arr.get(items, index), (item) => item.disabled);\n\nexport interface MenuBarDismiss {\n  readonly state: MenuBarState;\n\n  readonly refocus: Option.Option<number>;\n}\n\nexport const dismiss = (state: MenuBarState): MenuBarDismiss =>\n  Option.match(state.openMenu, {\n    onSome: (menu) => ({\n      state: {\n        activeMenu: Option.some(menu),\n        openMenu: Option.none(),\n        activeItem: Option.none(),\n      },\n      refocus: Option.some(menu),\n    }),\n    onNone: () => ({ state: initial, refocus: Option.none() }),\n  });\n\nexport const step = (\n  state: MenuBarState,\n  menus: ReadonlyArray<AppMenuSpec>,\n  action: MenuBarAction,\n): MenuBarState =>\n  Match.value(action).pipe(\n    Match.when({ _tag: \"MoveBar\" }, ({ delta }) =>\n      Option.match(state.openMenu, {\n        onSome: () => {\n          const next = moveIn(\n            state.openMenu,\n            Arr.length(menus),\n            delta,\n            \"horizontal\",\n            (i) => menuIsDisabled(menus, i),\n          );\n          return Option.match(next, {\n            onNone: () => state,\n            onSome: (menu) => ({\n              activeMenu: Option.some(menu),\n              openMenu: Option.some(menu),\n              activeItem: Option.none(),\n            }),\n          });\n        },\n        onNone: () => {\n          const next = moveIn(\n            state.activeMenu,\n            Arr.length(menus),\n            delta,\n            \"horizontal\",\n            (i) => menuIsDisabled(menus, i),\n          );\n          return Option.match(next, {\n            onNone: () => state,\n            onSome: (menu) => ({ ...state, activeMenu: Option.some(menu) }),\n          });\n        },\n      }),\n    ),\n    Match.when({ _tag: \"Open\" }, ({ menu }) => ({\n      activeMenu: Option.some(menu),\n      openMenu: Option.some(menu),\n      activeItem: Option.none(),\n    })),\n    Match.when({ _tag: \"MoveItem\" }, ({ delta }) =>\n      Option.match(state.openMenu, {\n        onNone: () => state,\n        onSome: (menu) => {\n          const items = itemsOf(menus, menu);\n          const next = moveIn(\n            state.activeItem,\n            Arr.length(items),\n            delta,\n            \"vertical\",\n            (i) => itemIsDisabled(items, i),\n          );\n          return Option.match(next, {\n            onNone: () => state,\n            onSome: (item) => ({ ...state, activeItem: Option.some(item) }),\n          });\n        },\n      }),\n    ),\n    Match.when({ _tag: \"Dismiss\" }, () => dismiss(state).state),\n    Match.exhaustive,\n  );\n\nexport const hoverOpen = (\n  state: MenuBarState,\n  menus: ReadonlyArray<AppMenuSpec>,\n  menu: number,\n): MenuBarState =>\n  Option.exists(state.openMenu, (open) => open !== menu) &&\n  Option.exists(Arr.get(menus, menu), (spec) => !spec.disabled)\n    ? step(state, menus, { _tag: \"Open\", menu })\n    : state;\n"
    },
    {
      "path": "packages/shell/src/recipe.ts",
      "target": "@ui/gpuixui/shell/recipe.ts",
      "type": "registry:lib",
      "content": "import type { StyleDesc } from \"@gpuix/react\";\nimport { classesToStyle } from \"../tw/index.js\";\nimport * as MutableHashMap from \"effect/MutableHashMap\";\nimport * as Option from \"effect/Option\";\nimport type { Theme } from \"./theme.js\";\n\nconst CACHE_LIMIT = 256;\n\nconst compiled = new WeakMap<\n  Theme,\n  MutableHashMap.MutableHashMap<string, StyleDesc>\n>();\n\nexport const recipeStyle = (recipe: string, theme: Theme): StyleDesc => {\n  const byRecipe = compiled.get(theme);\n  if (byRecipe !== undefined) {\n    const hit = MutableHashMap.get(byRecipe, recipe);\n    if (Option.isSome(hit)) return hit.value;\n  }\n  const style = Object.freeze(classesToStyle(recipe, theme).style);\n  if (byRecipe === undefined) {\n    compiled.set(theme, MutableHashMap.make([recipe, style]));\n  } else if (MutableHashMap.size(byRecipe) < CACHE_LIMIT) {\n    MutableHashMap.set(byRecipe, recipe, style);\n  }\n  return style;\n};\n"
    },
    {
      "path": "packages/shell/src/theme.ts",
      "target": "@ui/gpuixui/shell/theme.ts",
      "type": "registry:lib",
      "content": "import type { Theme } from \"../tokens/index.js\";\nimport { useTheme as useTokenTheme } from \"../tokens/react.js\";\n\nexport {\n  currentPlatform,\n  disabledOpacity,\n  materialFill,\n  statusTone,\n  textStyle,\n} from \"../tokens/index.js\";\nexport type {\n  PlatformName,\n  StatusTone,\n  TextRole,\n  Theme,\n} from \"../tokens/index.js\";\n\nexport const useTheme = (): Theme => useTokenTheme();\n"
    }
  ],
  "docs": "[Application menu](https://gpuixui.sudokw.com/shell/app-menu) — These are GPUix native components: the files render through GPUix's reconciler against `@gpuix/react`, not the DOM, and need the per-platform `@gpuix/native` binding. The CLI installs the `@gpuixui/*` runtime the components import as registry items, so the copied tree resolves on its own; wrap it in `ThemeProvider` before it renders.",
  "meta": {
    "url": "/shell/app-menu",
    "kind": "component",
    "runtime": "gpuix",
    "registry": "gpuixui"
  }
}