{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ink/menu",
  "title": "Menu",
  "description": "Dropdown menu with nested submenus",
  "dependencies": [
    "ink"
  ],
  "registryDependencies": [
    "https://termcn.dev/r/ink/interaction.json",
    "https://termcn.dev/r/ink/terminal-style.json",
    "https://termcn.dev/r/ink/use-theme.json",
    "https://termcn.dev/r/ink/use-unicode.json"
  ],
  "files": [
    {
      "path": "registry/ui/menu.tsx",
      "content": "import { Box, Text } from \"ink\";\nimport React, { useState } from \"react\";\n\nimport { useInteraction } from \"@/hooks/use-interaction\";\nimport { useTheme } from \"@/hooks/use-theme\";\nimport { useUnicode } from \"@/hooks/use-unicode\";\nimport { resolveBorderStyle } from \"@/lib/terminal-style\";\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  id?: string;\n  autoFocus?: boolean;\n  isActive?: boolean;\n  disabled?: boolean;\n  \"aria-label\"?: string;\n}\n\nexport const Menu = ({\n  items,\n  onSelect,\n  title,\n  id,\n  autoFocus = false,\n  isActive = true,\n  disabled = false,\n  \"aria-label\": ariaLabel,\n}: MenuProps) => {\n  const unicode = useUnicode();\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  const { isFocused: isMenuFocused } = useInteraction(\n    (_input, key) => {\n      if (key.upArrow) {\n        moveFocus(-1);\n      } else if (key.downArrow) {\n        moveFocus(1);\n      } else if (key.return) {\n        const item = activeItems[focusIndex];\n        if (item) {\n          activateItem(item);\n        }\n      } else if (key.rightArrow) {\n        const item = activeItems[focusIndex];\n        if (item?.children && item.children.length > 0) {\n          openSubmenu(item);\n        }\n      } else if (key.leftArrow) {\n        closeSubmenu();\n      } else if (key.escape) {\n        closeSubmenu();\n      } else if (key.home) {\n        setFocusIndex(selectableIndices[0] ?? 0);\n      } else if (key.end) {\n        setFocusIndex(selectableIndices.at(-1) ?? 0);\n      }\n    },\n    { autoFocus, disabled, id, isActive }\n  );\n\n  const depth = submenuStack.length;\n\n  return (\n    <Box\n      aria-role=\"menu\"\n      aria-state={{ disabled }}\n      flexDirection=\"column\"\n      borderStyle={resolveBorderStyle(\"single\", unicode)}\n      borderColor={theme.colors.border}\n      paddingX={1}\n      paddingY={0}\n      marginLeft={depth * 2}\n    >\n      {(title || ariaLabel) && (\n        <Box marginBottom={1}>\n          <Text\n            aria-label={ariaLabel ?? title}\n            bold\n            color={theme.colors.primary}\n          >\n            {title ?? \"\"}\n          </Text>\n        </Box>\n      )}\n      {activeItems.map((item, idx) => {\n        if (item.separator) {\n          return (\n            <Box key={item.key} aria-hidden>\n              <Text color={theme.colors.mutedForeground}>\n                {(unicode ? \"─\" : \"-\").repeat(20)}\n              </Text>\n            </Box>\n          );\n        }\n\n        const isFocused = isMenuFocused && idx === focusIndex;\n\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\n        return (\n          <Box\n            key={item.key}\n            aria-label={\n              item.shortcut ? `${item.label}, ${item.shortcut}` : item.label\n            }\n            aria-role=\"menuitem\"\n            aria-state={{\n              disabled: disabled || item.disabled,\n              expanded:\n                item.children && item.children.length > 0\n                  ? submenuStack.at(-1) === item.children\n                  : undefined,\n              selected: isFocused,\n            }}\n            justifyContent=\"space-between\"\n          >\n            <Box aria-hidden flexDirection=\"row\" gap={1}>\n              <Text color={isFocused ? theme.colors.primary : \"transparent\"}>\n                {isFocused ? (unicode ? \"›\" : \">\") : \" \"}\n              </Text>\n              {item.icon && (\n                <Text\n                  color={\n                    item.disabled\n                      ? theme.colors.mutedForeground\n                      : theme.colors.foreground\n                  }\n                >\n                  {item.icon}\n                </Text>\n              )}\n              <Text\n                color={labelColor}\n                bold={isFocused && !item.disabled}\n                dimColor={item.disabled}\n              >\n                {item.label}\n              </Text>\n              {item.children && item.children.length > 0 && (\n                <Text color={theme.colors.mutedForeground}>\n                  {unicode ? \"›\" : \">\"}\n                </Text>\n              )}\n            </Box>\n            {item.shortcut && (\n              <Text aria-hidden color={theme.colors.mutedForeground}>\n                {item.shortcut}\n              </Text>\n            )}\n          </Box>\n        );\n      })}\n    </Box>\n  );\n};\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "navigation"
  ],
  "type": "registry:ui"
}