{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tag-input",
  "type": "registry:ui",
  "description": "Multi-tag input with suggestions, validation, and keyboard support",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/default/ui/tag-input.tsx",
      "content": "import * as React from 'react'\nimport { cn } from '@/lib/utils'\nimport { X } from 'lucide-react'\n\nexport interface TagInputProps\n  extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'value' | 'defaultValue' | 'onChange'> {\n  value?: string[]\n  defaultValue?: string[]\n  onChange?: (tags: string[]) => void\n  suggestions?: string[]\n  maxTags?: number\n  allowDuplicates?: boolean\n  delimiter?: string | RegExp\n  validateTag?: (tag: string) => boolean | string\n}\n\nconst TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(\n  (\n    {\n      value: controlledValue,\n      defaultValue = [],\n      onChange,\n      suggestions = [],\n      maxTags,\n      allowDuplicates = false,\n      delimiter = ',',\n      validateTag,\n      placeholder = 'Add tag...',\n      disabled,\n      className,\n      ...props\n    },\n    ref\n  ) => {\n    const [uncontrolledTags, setUncontrolledTags] = React.useState<string[]>(defaultValue)\n    const [inputValue, setInputValue] = React.useState('')\n    const [showSuggestions, setShowSuggestions] = React.useState(false)\n    const [error, setError] = React.useState<string | null>(null)\n    const [selectedSuggestionIndex, setSelectedSuggestionIndex] = React.useState(-1)\n\n    const inputRef = React.useRef<HTMLInputElement>(null)\n    const containerRef = React.useRef<HTMLDivElement>(null)\n\n    React.useImperativeHandle(ref, () => inputRef.current!)\n\n    const isControlled = controlledValue !== undefined\n    const tags = isControlled ? controlledValue : uncontrolledTags\n\n    const filteredSuggestions = React.useMemo(() => {\n      if (!inputValue.trim() || suggestions.length === 0) return []\n      const lowerInput = inputValue.toLowerCase()\n      return suggestions.filter(\n        (suggestion) =>\n          suggestion.toLowerCase().includes(lowerInput) &&\n          (allowDuplicates || !tags.includes(suggestion))\n      )\n    }, [inputValue, suggestions, tags, allowDuplicates])\n\n    const updateTags = (newTags: string[]) => {\n      if (!isControlled) {\n        setUncontrolledTags(newTags)\n      }\n      onChange?.(newTags)\n    }\n\n    const addTag = (tagValue: string) => {\n      const trimmedTag = tagValue.trim()\n      if (!trimmedTag) return false\n\n      // Check max tags\n      if (maxTags && tags.length >= maxTags) {\n        setError(`Maximum ${maxTags} tags allowed`)\n        return false\n      }\n\n      // Check duplicates\n      if (!allowDuplicates && tags.includes(trimmedTag)) {\n        setError('Tag already exists')\n        return false\n      }\n\n      // Validate tag\n      if (validateTag) {\n        const validationResult = validateTag(trimmedTag)\n        if (validationResult !== true) {\n          setError(typeof validationResult === 'string' ? validationResult : 'Invalid tag')\n          return false\n        }\n      }\n\n      setError(null)\n      updateTags([...tags, trimmedTag])\n      return true\n    }\n\n    const removeTag = (index: number) => {\n      if (disabled) return\n      const newTags = tags.filter((_, i) => i !== index)\n      updateTags(newTags)\n      setError(null)\n    }\n\n    const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n      const value = e.target.value\n      setInputValue(value)\n      setShowSuggestions(true)\n      setSelectedSuggestionIndex(-1)\n      setError(null)\n\n      // Check for delimiter\n      if (delimiter) {\n        const parts = typeof delimiter === 'string'\n          ? value.split(delimiter)\n          : value.split(delimiter)\n\n        if (parts.length > 1) {\n          const newTags = parts.slice(0, -1).filter((part) => part.trim())\n          newTags.forEach((tag) => addTag(tag))\n          setInputValue(parts[parts.length - 1])\n        }\n      }\n    }\n\n    const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {\n      switch (e.key) {\n        case 'Enter':\n          e.preventDefault()\n          if (selectedSuggestionIndex >= 0 && filteredSuggestions[selectedSuggestionIndex]) {\n            if (addTag(filteredSuggestions[selectedSuggestionIndex])) {\n              setInputValue('')\n              setShowSuggestions(false)\n              setSelectedSuggestionIndex(-1)\n            }\n          } else if (inputValue.trim()) {\n            if (addTag(inputValue)) {\n              setInputValue('')\n            }\n          }\n          break\n        case 'Backspace':\n          if (!inputValue && tags.length > 0) {\n            removeTag(tags.length - 1)\n          }\n          break\n        case 'ArrowDown':\n          if (filteredSuggestions.length > 0) {\n            e.preventDefault()\n            setSelectedSuggestionIndex((prev) =>\n              prev < filteredSuggestions.length - 1 ? prev + 1 : 0\n            )\n          }\n          break\n        case 'ArrowUp':\n          if (filteredSuggestions.length > 0) {\n            e.preventDefault()\n            setSelectedSuggestionIndex((prev) =>\n              prev > 0 ? prev - 1 : filteredSuggestions.length - 1\n            )\n          }\n          break\n        case 'Escape':\n          setShowSuggestions(false)\n          setSelectedSuggestionIndex(-1)\n          break\n      }\n    }\n\n    const handleSuggestionClick = (suggestion: string) => {\n      if (addTag(suggestion)) {\n        setInputValue('')\n        setShowSuggestions(false)\n        inputRef.current?.focus()\n      }\n    }\n\n    const handleContainerClick = () => {\n      inputRef.current?.focus()\n    }\n\n    // Close suggestions on click outside\n    React.useEffect(() => {\n      const handleClickOutside = (e: MouseEvent) => {\n        if (containerRef.current && !containerRef.current.contains(e.target as Node)) {\n          setShowSuggestions(false)\n        }\n      }\n      document.addEventListener('mousedown', handleClickOutside)\n      return () => document.removeEventListener('mousedown', handleClickOutside)\n    }, [])\n\n    return (\n      <div ref={containerRef} className=\"relative\">\n        <div\n          onClick={handleContainerClick}\n          className={cn(\n            'flex flex-wrap items-center gap-2 min-h-11 w-full border-3 border-input bg-background px-3 py-2',\n            'shadow-[4px_4px_0px_hsl(var(--shadow-color))] transition-all duration-200',\n            'focus-within:translate-x-[4px] focus-within:translate-y-[4px] focus-within:shadow-none',\n            disabled && 'opacity-50 cursor-not-allowed',\n            error && 'border-destructive',\n            className\n          )}\n        >\n          {/* Tags */}\n          {tags.map((tag, index) => (\n            <span\n              key={`${tag}-${index}`}\n              className={cn(\n                'inline-flex items-center gap-1 px-2 py-0.5 text-xs font-bold uppercase tracking-wide',\n                'border-2 border-foreground bg-primary text-primary-foreground',\n                'shadow-[2px_2px_0px_hsl(var(--shadow-color))]'\n              )}\n            >\n              {tag}\n              {!disabled && (\n                <button\n                  type=\"button\"\n                  onClick={(e) => {\n                    e.stopPropagation()\n                    removeTag(index)\n                  }}\n                  className=\"hover:bg-primary-foreground/20 rounded-sm p-0.5 transition-colors\"\n                  aria-label={`Remove ${tag}`}\n                >\n                  <X className=\"h-3 w-3\" />\n                </button>\n              )}\n            </span>\n          ))}\n\n          {/* Input */}\n          <input\n            ref={inputRef}\n            type=\"text\"\n            value={inputValue}\n            onChange={handleInputChange}\n            onKeyDown={handleKeyDown}\n            onFocus={() => setShowSuggestions(true)}\n            placeholder={tags.length === 0 ? placeholder : ''}\n            disabled={disabled}\n            className={cn(\n              'flex-1 min-w-[120px] bg-transparent outline-none text-sm',\n              'placeholder:text-muted-foreground disabled:cursor-not-allowed'\n            )}\n            {...props}\n          />\n        </div>\n\n        {/* Error message */}\n        {error && (\n          <p className=\"mt-1 text-xs font-medium text-destructive\">{error}</p>\n        )}\n\n        {/* Suggestions dropdown */}\n        {showSuggestions && filteredSuggestions.length > 0 && (\n          <div\n            className={cn(\n              'absolute z-50 mt-1 w-full',\n              'border-3 border-foreground bg-popover',\n              'shadow-[4px_4px_0px_hsl(var(--shadow-color))]'\n            )}\n          >\n            {filteredSuggestions.map((suggestion, index) => (\n              <button\n                key={suggestion}\n                type=\"button\"\n                onClick={() => handleSuggestionClick(suggestion)}\n                className={cn(\n                  'w-full px-3 py-2 text-left text-sm transition-colors',\n                  'hover:bg-muted',\n                  index === selectedSuggestionIndex && 'bg-accent'\n                )}\n              >\n                {suggestion}\n              </button>\n            ))}\n          </div>\n        )}\n      </div>\n    )\n  }\n)\nTagInput.displayName = 'TagInput'\n\nexport { TagInput }\n",
      "type": "registry:ui",
      "target": "components/ui/tag-input.tsx"
    }
  ]
}