{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "opentui/menu",
  "title": "Menu",
  "description": "Dropdown menu with nested submenus",
  "dependencies": [
    "@opentui/react"
  ],
  "registryDependencies": [
    "https://termcn.dev/r/opentui/use-theme.json"
  ],
  "files": [
    {
      "path": "registry/opentui/ui/menu.tsx",
      "content": "/* @jsxImportSource @opentui/react */\nimport { useKeyboard } from \"@opentui/react\";\nimport { useState } from \"react\";\n\nimport { useTheme } from \"@/hooks/use-theme\";\n\nexport interface MenuItem {\n  key: string;\n  label: string;\n  icon?: string;\n  shortcut?: string;\n  disabled?: boolean;\n  separator?: boolean;\n  children?: MenuItem[];\n}\n\nexport interface MenuProps {\n  items: MenuItem[];\n  onSelect?: (item: MenuItem) => void;\n  title?: string;\n}\n\nexport const Menu = ({ items, onSelect, title }: MenuProps) => {\n  const theme = useTheme();\n  const [focusIndex, setFocusIndex] = useState(0);\n  const [submenuStack, setSubmenuStack] = useState<MenuItem[][]>([]);\n\n  const activeItems = submenuStack.at(-1) ?? items;\n\n  const selectableIndices = activeItems\n    .map((item, idx) => ({ idx, item }))\n    .filter(({ item }) => !item.separator && !item.disabled)\n    .map(({ idx }) => idx);\n\n  const moveFocus = (direction: 1 | -1) => {\n    const currentPos = selectableIndices.indexOf(focusIndex);\n    const nextPos = currentPos + direction;\n    if (nextPos >= 0 && nextPos < selectableIndices.length) {\n      setFocusIndex(selectableIndices[nextPos]);\n    }\n  };\n\n  const openSubmenu = (item: MenuItem) => {\n    if (item.children && item.children.length > 0) {\n      const { children } = item;\n      setSubmenuStack((prev) => [...prev, children]);\n      setFocusIndex(0);\n    }\n  };\n\n  const closeSubmenu = () => {\n    if (submenuStack.length > 0) {\n      setSubmenuStack((prev) => prev.slice(0, -1));\n      setFocusIndex(0);\n    }\n  };\n\n  const activateItem = (item: MenuItem) => {\n    if (item.disabled || item.separator) {\n      return;\n    }\n    if (item.children && item.children.length > 0) {\n      openSubmenu(item);\n    } else {\n      onSelect?.(item);\n    }\n  };\n\n  useKeyboard((key) => {\n    if (key.name === \"up\") {\n      moveFocus(-1);\n    } else if (key.name === \"down\") {\n      moveFocus(1);\n    } else if (key.name === \"return\") {\n      const item = activeItems[focusIndex];\n      if (item) {\n        activateItem(item);\n      }\n    } else if (key.name === \"right\") {\n      const item = activeItems[focusIndex];\n      if (item?.children && item.children.length > 0) {\n        openSubmenu(item);\n      }\n    } else if (key.name === \"left\") {\n      closeSubmenu();\n    } else if (key.name === \"escape\") {\n      closeSubmenu();\n    }\n  });\n\n  const depth = submenuStack.length;\n\n  return (\n    <box\n      flexDirection=\"column\"\n      borderStyle=\"single\"\n      borderColor={theme.colors.border}\n      paddingLeft={1}\n      paddingRight={1}\n      paddingTop={0}\n      paddingBottom={0}\n      marginLeft={depth * 2}\n    >\n      {title && (\n        <box marginBottom={1}>\n          <text fg={theme.colors.primary}>\n            <b>{title}</b>\n          </text>\n        </box>\n      )}\n      {activeItems.map((item, idx) => {\n        if (item.separator) {\n          return (\n            <box key={item.key}>\n              <text fg={theme.colors.mutedForeground}>{\"─\".repeat(20)}</text>\n            </box>\n          );\n        }\n        const isFocused = idx === focusIndex;\n        let labelColor: string;\n        if (item.disabled) {\n          labelColor = theme.colors.mutedForeground;\n        } else if (isFocused) {\n          labelColor = theme.colors.primary;\n        } else {\n          labelColor = theme.colors.foreground;\n        }\n        return (\n          <box key={item.key} justifyContent=\"space-between\">\n            <box flexDirection=\"row\" gap={1}>\n              <text fg={isFocused ? theme.colors.primary : \"transparent\"}>\n                {isFocused ? \"›\" : \" \"}\n              </text>\n              {item.icon && (\n                <text\n                  fg={\n                    item.disabled\n                      ? theme.colors.mutedForeground\n                      : theme.colors.foreground\n                  }\n                >\n                  {item.icon}\n                </text>\n              )}\n              <text fg={labelColor}>\n                {isFocused && !item.disabled ? <b>{item.label}</b> : item.label}\n              </text>\n              {item.children && item.children.length > 0 && (\n                <text fg={theme.colors.mutedForeground}>›</text>\n              )}\n            </box>\n            {item.shortcut && (\n              <text fg={theme.colors.mutedForeground}>{item.shortcut}</text>\n            )}\n          </box>\n        );\n      })}\n    </box>\n  );\n};\n",
      "type": "registry:ui",
      "target": "components/ui/menu.tsx"
    }
  ],
  "categories": [
    "navigation"
  ],
  "type": "registry:ui"
}