{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ink/list",
  "title": "List",
  "description": "Navigable list with optional filtering",
  "dependencies": [
    "ink"
  ],
  "registryDependencies": [
    "https://termcn.dev/r/ink/interaction.json",
    "https://termcn.dev/r/ink/terminal-style.json",
    "https://termcn.dev/r/ink/terminal-symbols.json",
    "https://termcn.dev/r/ink/terminal-text.json",
    "https://termcn.dev/r/ink/use-theme.json",
    "https://termcn.dev/r/ink/use-unicode.json"
  ],
  "files": [
    {
      "path": "registry/ui/list.tsx",
      "content": "import { Box, Text } from \"ink\";\nimport React, { useState, useMemo } 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 { resolveBorderStyle } from \"@/lib/terminal-style\";\nimport { resolveTerminalSymbol } from \"@/lib/terminal-symbols\";\nimport { removeGraphemeBefore } from \"@/lib/terminal-text\";\n\nexport interface ListItem {\n  key: string;\n  label: string;\n  description?: string;\n  color?: string;\n  disabled?: boolean;\n}\n\nexport interface ListProps extends InteractionProps {\n  items: ListItem[];\n  onSelect?: (item: ListItem) => void;\n  filterable?: boolean;\n  height?: number;\n  cursor?: string;\n  \"aria-label\"?: string;\n}\n\nexport const List = ({\n  items,\n  onSelect,\n  filterable = false,\n  height = 10,\n  cursor,\n  id,\n  autoFocus,\n  isActive,\n  disabled,\n  \"aria-label\": ariaLabel = \"List\",\n}: ListProps) => {\n  const unicode = useUnicode();\n  const theme = useTheme();\n  const resolvedCursor = cursor ?? resolveTerminalSymbol(unicode, \"›\", \">\");\n  const [activeIndex, setActiveIndex] = useState(0);\n  const [filter, setFilter] = useState(\"\");\n\n  const filtered = useMemo(() => {\n    if (!filter) {\n      return items;\n    }\n    const q = filter.toLowerCase();\n    return items.filter((item) => item.label.toLowerCase().includes(q));\n  }, [items, filter]);\n\n  const { isFocused } = useInteraction(\n    (input, key) => {\n      const enabled = filtered\n        .map((item, index) => ({ index, item }))\n        .filter(({ item }) => !item.disabled);\n      const currentEnabledIndex = enabled.findIndex(\n        ({ index }) => index === activeIndex\n      );\n      if (key.upArrow) {\n        setActiveIndex(\n          enabled[Math.max(0, currentEnabledIndex - 1)]?.index ?? 0\n        );\n      } else if (key.downArrow) {\n        setActiveIndex(\n          enabled[Math.min(enabled.length - 1, currentEnabledIndex + 1)]\n            ?.index ?? 0\n        );\n      } else if (key.home) {\n        setActiveIndex(enabled[0]?.index ?? 0);\n      } else if (key.end) {\n        setActiveIndex(enabled.at(-1)?.index ?? 0);\n      } else if (key.return) {\n        const item = filtered[activeIndex];\n        if (item && !item.disabled) {\n          onSelect?.(item);\n        }\n      } else if (filterable && key.backspace) {\n        setFilter(\n          (f) => removeGraphemeBefore(f, Number.POSITIVE_INFINITY).value\n        );\n      } else if (\n        filterable &&\n        !key.escape &&\n        !key.return &&\n        !key.upArrow &&\n        !key.downArrow\n      ) {\n        setFilter((f) => f + input);\n      }\n    },\n    { autoFocus, disabled, id, isActive }\n  );\n\n  const visible = filtered.slice(0, height);\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      {filterable && (\n        <Box\n          borderStyle={resolveBorderStyle(\"round\", unicode)}\n          borderColor={theme.colors.border}\n          paddingX={1}\n          marginBottom={1}\n        >\n          <Text dimColor={!filter}>\n            {filter ||\n              resolveTerminalSymbol(\n                unicode,\n                \"Type to filter…\",\n                \"Type to filter...\"\n              )}\n          </Text>\n        </Box>\n      )}\n      {visible.map((item, idx) => {\n        const isActive = idx === activeIndex;\n        return (\n          <Box\n            key={item.key}\n            gap={1}\n            aria-role=\"option\"\n            aria-label={`${item.label}${item.description ? `. ${item.description}` : \"\"}`}\n            aria-state={{\n              disabled: item.disabled || undefined,\n              selected: isActive && isFocused,\n            }}\n          >\n            <Text color={isActive ? theme.colors.primary : undefined}>\n              {isActive ? `[${resolvedCursor}]` : \" \"}\n            </Text>\n            <Text\n              color={\n                item.color ??\n                (isActive ? theme.colors.primary : theme.colors.foreground)\n              }\n              bold={isActive}\n            >\n              {item.label}\n            </Text>\n            {item.description && (\n              <Text color={theme.colors.mutedForeground} dimColor>\n                {item.description}\n              </Text>\n            )}\n          </Box>\n        );\n      })}\n      {filtered.length > height && (\n        <Text color={theme.colors.mutedForeground} dimColor>\n          {filtered.length - height}{\" \"}\n          {resolveTerminalSymbol(unicode, \"more…\", \"more...\")}\n        </Text>\n      )}\n    </Box>\n  );\n};\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "data"
  ],
  "type": "registry:ui"
}