{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ink/tag-input",
  "title": "Tag Input",
  "description": "Add and remove tags inline",
  "dependencies": [
    "ink"
  ],
  "registryDependencies": [
    "https://termcn.dev/r/ink/interaction.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/tag-input.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 { resolveTerminalSymbol } from \"@/lib/terminal-symbols\";\nimport {\n  graphemeLength,\n  removeGraphemeBefore,\n} from \"@/lib/terminal-text\";\n\nexport interface TagInputProps {\n  value?: string[];\n  defaultValue?: string[];\n  onValueChange?: (tags: string[]) => void;\n  onChange?: (tags: string[]) => void;\n  placeholder?: string;\n  maxTags?: number;\n  delimiter?: string;\n  label?: string;\n  id?: string;\n  autoFocus?: boolean;\n  isActive?: boolean;\n  disabled?: boolean;\n  readOnly?: boolean;\n  required?: boolean;\n  \"aria-label\"?: string;\n}\n\nexport const TagInput = ({\n  value: controlledValue,\n  defaultValue = [],\n  onValueChange,\n  onChange,\n  placeholder,\n  maxTags,\n  delimiter = \",\",\n  label,\n  id,\n  autoFocus = false,\n  isActive = true,\n  disabled = false,\n  readOnly = false,\n  required = false,\n  \"aria-label\": ariaLabel,\n}: TagInputProps) => {\n  const theme = useTheme();\n  const unicode = useUnicode();\n  const resolvedPlaceholder =\n    placeholder ??\n    resolveTerminalSymbol(\n      unicode,\n      \"Type and press Enter…\",\n      \"Type and press Enter...\"\n    );\n  const [internalTags, setInternalTags] = useState<string[]>(defaultValue);\n  const [inputText, setInputText] = useState(\"\");\n\n  const tags = controlledValue ?? internalTags;\n\n  const updateTags = (next: string[]) => {\n    if (controlledValue === undefined) {\n      setInternalTags(next);\n    }\n    (onValueChange ?? onChange)?.(next);\n  };\n\n  const addTag = () => {\n    const trimmed = inputText.trim();\n    if (!trimmed) {\n      return;\n    }\n    if (maxTags !== undefined && tags.length >= maxTags) {\n      return;\n    }\n    updateTags([...tags, trimmed]);\n    setInputText(\"\");\n  };\n\n  const removeLastTag = () => {\n    if (tags.length === 0) {\n      return;\n    }\n    updateTags(tags.slice(0, -1));\n  };\n\n  const { isFocused } = useInteraction(\n    (input, key) => {\n      if (readOnly) {\n        return;\n      }\n      if (key.return) {\n        addTag();\n      } else if (key.backspace) {\n        if (graphemeLength(inputText) > 0) {\n          setInputText(\n            (current) =>\n              removeGraphemeBefore(current, graphemeLength(current)).value\n          );\n        } else {\n          removeLastTag();\n        }\n      } else if (input === delimiter) {\n        addTag();\n      } else if (\n        input &&\n        !key.ctrl &&\n        !key.meta &&\n        !key.upArrow &&\n        !key.downArrow &&\n        !key.leftArrow &&\n        !key.rightArrow &&\n        !key.escape &&\n        !key.tab\n      ) {\n        setInputText((t) => t + input);\n      }\n    },\n    { autoFocus, disabled, id, isActive }\n  );\n\n  const atMax = maxTags !== undefined && tags.length >= maxTags;\n\n  return (\n    <Box flexDirection=\"column\" gap={1}>\n      <Text\n        aria-label={ariaLabel ?? label ?? \"Tag input\"}\n        bold={Boolean(label)}\n      >\n        {label ?? \"\"}\n      </Text>\n      <Box aria-role=\"list\" flexWrap=\"wrap\" gap={1}>\n        {tags.map((tag, idx) => (\n          <Box key={`${tag}-${idx}`} aria-label={tag} aria-role=\"listitem\">\n            <Text aria-hidden bold>\n              [\n            </Text>\n            <Text>{tag}</Text>\n            <Text aria-hidden bold>\n              {\" \"}\n              {resolveTerminalSymbol(unicode, \"×]\", \"x]\")}\n            </Text>\n          </Box>\n        ))}\n      </Box>\n      <Box\n        aria-label={`${ariaLabel ?? label ?? \"Tag input\"}: ${inputText || \"empty\"}. ${tags.length} tags`}\n        aria-role=\"textbox\"\n        aria-state={{ disabled, readonly: readOnly, required }}\n        gap={1}\n      >\n        <Text aria-hidden color={theme.colors.primary}>\n          {isFocused ? \">\" : \" \"}\n        </Text>\n        {atMax ? (\n          <Text color={theme.colors.mutedForeground} dimColor>\n            {`Max ${maxTags} tag${maxTags === 1 ? \"\" : \"s\"} reached`}\n          </Text>\n        ) : (\n          <Text\n            color={\n              inputText ? theme.colors.foreground : theme.colors.mutedForeground\n            }\n          >\n            {inputText || resolvedPlaceholder}\n          </Text>\n        )}\n      </Box>\n    </Box>\n  );\n};\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "selection"
  ],
  "type": "registry:ui"
}