{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ink/model-selector",
  "title": "Model Selector",
  "description": "Keyboard-navigable list for selecting an AI model, with optional provider grouping and context-window display",
  "dependencies": [
    "ink"
  ],
  "registryDependencies": [
    "https://termcn.dev/r/ink/interaction.json",
    "https://termcn.dev/r/ink/terminal-symbols.json",
    "https://termcn.dev/r/ink/use-theme.json",
    "https://termcn.dev/r/ink/use-unicode.json"
  ],
  "files": [
    {
      "path": "registry/ui/model-selector.tsx",
      "content": "import { Box, Text } from \"ink\";\nimport React, { useEffect, useState } from \"react\";\n\nimport { useInteraction } from \"@/hooks/use-interaction\";\nimport type { InteractionProps } from \"@/hooks/use-interaction\";\nimport { useTheme } from \"@/hooks/use-theme\";\nimport { useUnicode } from \"@/hooks/use-unicode\";\nimport {\n  resolveStatusSymbol,\n  resolveTerminalSymbol,\n} from \"@/lib/terminal-symbols\";\n\nexport interface ModelOption {\n  id: string;\n  name: string;\n  provider: string;\n  context?: number;\n  disabled?: boolean;\n}\n\nexport interface ModelSelectorProps extends InteractionProps {\n  models: ModelOption[];\n  value?: string;\n  defaultValue?: string;\n  selected?: string;\n  onValueChange?: (id: string) => void;\n  onSelect?: (id: string) => void;\n  showContext?: boolean;\n  showProvider?: boolean;\n  groupByProvider?: boolean;\n  \"aria-label\"?: string;\n}\n\nconst formatContext = (ctx: number): string => {\n  if (ctx >= 1_000_000) {\n    return `${(ctx / 1_000_000).toFixed(0)}M ctx`;\n  }\n  if (ctx >= 1000) {\n    return `${(ctx / 1000).toFixed(0)}k ctx`;\n  }\n  return `${ctx} ctx`;\n};\n\ninterface ModelRowProps {\n  model: ModelOption;\n  isActive: boolean;\n  isSelected: boolean;\n  showContext: boolean;\n  showProvider: boolean;\n  theme: ReturnType<typeof useTheme>;\n  controlFocused: boolean;\n  unicode: boolean;\n}\n\nconst getModelColor = (\n  isSelected: boolean,\n  isActive: boolean,\n  theme: ReturnType<typeof useTheme>\n): string => {\n  if (isSelected) {\n    return theme.colors.success ?? \"green\";\n  }\n  if (isActive) {\n    return theme.colors.primary;\n  }\n  return theme.colors.foreground;\n};\n\nconst ModelRow = ({\n  model,\n  isActive,\n  isSelected,\n  showContext,\n  showProvider,\n  theme,\n  controlFocused,\n  unicode,\n}: ModelRowProps) => (\n  <Box\n    gap={1}\n    aria-role=\"option\"\n    aria-label={`${model.name}, ${model.provider}${model.context === undefined ? \"\" : `, ${formatContext(model.context)}`}`}\n    aria-state={{ disabled: model.disabled || undefined, selected: isSelected }}\n  >\n    <Text color={isActive ? theme.colors.primary : undefined}>\n      {isActive && controlFocused\n        ? `[${resolveTerminalSymbol(unicode, \"›\", \">\")}]`\n        : \" \"}\n    </Text>\n    <Text\n      bold={isActive || isSelected}\n      color={getModelColor(isSelected, isActive, theme)}\n    >\n      {model.name}\n    </Text>\n    {isSelected && (\n      <Text color={theme.colors.success ?? \"green\"}>\n        {resolveStatusSymbol(unicode, \"success\")}\n      </Text>\n    )}\n    {showProvider && (\n      <Text dimColor color={theme.colors.mutedForeground}>\n        {model.provider}\n      </Text>\n    )}\n    {showContext && model.context !== undefined && (\n      <Text dimColor color={theme.colors.mutedForeground}>\n        {formatContext(model.context)}\n      </Text>\n    )}\n  </Box>\n);\n\nexport const ModelSelector = ({\n  models,\n  value,\n  defaultValue,\n  selected,\n  onValueChange,\n  onSelect,\n  showContext = true,\n  showProvider = true,\n  groupByProvider = false,\n  id,\n  autoFocus,\n  isActive,\n  disabled,\n  \"aria-label\": ariaLabel = \"Model selector\",\n}: ModelSelectorProps) => {\n  const theme = useTheme();\n  const unicode = useUnicode();\n  const [internalValue, setInternalValue] = useState(\n    defaultValue ?? selected ?? models[0]?.id ?? \"\"\n  );\n  const selectedId = value ?? selected ?? internalValue;\n  const [activeId, setActiveId] = useState(selectedId || models[0]?.id || \"\");\n  const activeIndex = Math.max(\n    0,\n    models.findIndex((model) => model.id === activeId)\n  );\n\n  const commit = (nextValue: string) => {\n    if (value === undefined && selected === undefined) {\n      setInternalValue(nextValue);\n    }\n    onValueChange?.(nextValue);\n    onSelect?.(nextValue);\n  };\n\n  const enabledModels = models.filter((model) => !model.disabled);\n  const { isFocused } = useInteraction(\n    (_input, key) => {\n      const enabledIndex = enabledModels.findIndex(\n        (model) => model.id === activeId\n      );\n      if (key.upArrow) {\n        setActiveId(enabledModels[Math.max(0, enabledIndex - 1)]?.id ?? \"\");\n      } else if (key.downArrow) {\n        setActiveId(\n          enabledModels[Math.min(enabledModels.length - 1, enabledIndex + 1)]\n            ?.id ?? \"\"\n        );\n      } else if (key.home) {\n        setActiveId(enabledModels[0]?.id ?? \"\");\n      } else if (key.end) {\n        setActiveId(enabledModels.at(-1)?.id ?? \"\");\n      } else if (key.return) {\n        const model = models.find((item) => item.id === activeId);\n        if (model && !model.disabled) {\n          commit(model.id);\n        }\n      }\n    },\n    { autoFocus, disabled, id, isActive }\n  );\n\n  useEffect(() => {\n    if (!models.some((model) => model.id === activeId && !model.disabled)) {\n      setActiveId(enabledModels[0]?.id ?? \"\");\n    }\n  }, [activeId, enabledModels, models]);\n\n  if (groupByProvider) {\n    const providerGroups: Record<string, ModelOption[]> = {\n      /* noop */\n    };\n    for (const m of models) {\n      if (!providerGroups[m.provider]) {\n        providerGroups[m.provider] = [];\n      }\n      providerGroups[m.provider]?.push(m);\n    }\n\n    return (\n      <Box\n        flexDirection=\"column\"\n        aria-role=\"listbox\"\n        aria-state={{ disabled: disabled || undefined }}\n      >\n        <Text aria-label={ariaLabel}>{\"\"}</Text>\n        {Object.entries(providerGroups).map(([provider, group]) => (\n          <Box key={provider} flexDirection=\"column\">\n            <Text bold color={theme.colors.primary}>\n              {provider}\n            </Text>\n            {group.map((model) => {\n              const globalIdx = models.indexOf(model);\n              const isActive = globalIdx === activeIndex;\n              const isSelected = model.id === selectedId;\n              return (\n                <ModelRow\n                  key={model.id}\n                  model={model}\n                  isActive={isActive}\n                  isSelected={isSelected}\n                  showContext={showContext}\n                  showProvider={false}\n                  theme={theme}\n                  controlFocused={isFocused}\n                  unicode={unicode}\n                />\n              );\n            })}\n          </Box>\n        ))}\n      </Box>\n    );\n  }\n\n  return (\n    <Box\n      flexDirection=\"column\"\n      aria-role=\"listbox\"\n      aria-state={{ disabled: disabled || undefined }}\n    >\n      <Text aria-label={ariaLabel}>{\"\"}</Text>\n      {models.map((model, idx) => {\n        const isActive = idx === activeIndex;\n        const isSelected = model.id === selectedId;\n        return (\n          <ModelRow\n            key={model.id}\n            model={model}\n            isActive={isActive}\n            isSelected={isSelected}\n            showContext={showContext}\n            showProvider={showProvider}\n            theme={theme}\n            controlFocused={isFocused}\n            unicode={unicode}\n          />\n        );\n      })}\n    </Box>\n  );\n};\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "ai"
  ],
  "type": "registry:ui"
}