{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tag-input",
  "title": "Tag Input",
  "description": "An input that turns what you typed into a removable tag.",
  "dependencies": [
    "lucide-react@^1.31.0"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/default/tag-input/tag-input.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { X } from \"lucide-react\"\nimport { cn } from \"@/lib/utils\"\n\nexport type TagInputProps = Omit<\n  React.HTMLAttributes<HTMLDivElement>,\n  \"onChange\" | \"defaultValue\"\n> & {\n  value?: readonly string[]\n  defaultValue?: readonly string[]\n  onChange?: (tags: string[]) => void\n  placeholder?: string\n  /** Keys that end a tag, besides Enter. */\n  separators?: readonly string[]\n  max?: number\n  /** Whether the same tag may be added twice. */\n  allowDuplicates?: boolean\n  label?: string\n  disabled?: boolean\n}\n\n/**\n * An input that turns what you typed into a removable tag. Backspace on an\n * empty field takes the last one back, which is the thing people try first.\n */\nexport function TagInput({\n  value,\n  defaultValue = [],\n  onChange,\n  placeholder = \"Add a tag\",\n  separators = [\",\", \"Enter\"],\n  max,\n  allowDuplicates = false,\n  label = \"Tags\",\n  disabled,\n  className,\n  ...rootProps\n}: TagInputProps) {\n  const [uncontrolled, setUncontrolled] = React.useState<string[]>([\n    ...defaultValue,\n  ])\n  const [draft, setDraft] = React.useState(\"\")\n  const [message, setMessage] = React.useState(\"\")\n  const inputRef = React.useRef<HTMLInputElement>(null)\n  const reactId = React.useId()\n\n  const tags = value ? [...value] : uncontrolled\n  const full = max !== undefined && tags.length >= max\n\n  const commit = (next: string[], said: string) => {\n    if (value === undefined) setUncontrolled(next)\n    onChange?.(next)\n    setMessage(said)\n  }\n\n  const add = (raw: string) => {\n    const tag = raw.trim()\n    if (tag === \"\" || full) return\n\n    if (!allowDuplicates && tags.includes(tag)) {\n      setMessage(`${tag} is already there`)\n      setDraft(\"\")\n      return\n    }\n\n    commit([...tags, tag], `Added ${tag}`)\n    setDraft(\"\")\n  }\n\n  const remove = (index: number) => {\n    const tag = tags[index]\n    if (tag === undefined) return\n    commit(\n      tags.filter((_, position) => position !== index),\n      `Removed ${tag}`\n    )\n  }\n\n  return (\n    <div\n      data-slot=\"tag-input\"\n      className={cn(\n        \"border-border bg-background focus-within:border-ring focus-within:ring-ring flex min-h-11 flex-wrap items-center gap-1.5 rounded-[var(--radius)] border p-1.5 focus-within:ring-2\",\n        disabled && \"opacity-50\",\n        className\n      )}\n      onPointerDown={(event) => {\n        if (event.target === event.currentTarget) inputRef.current?.focus()\n      }}\n      {...rootProps}\n    >\n      <span className=\"sr-only\" id={`${reactId}-label`}>\n        {label}\n      </span>\n\n      {tags.map((tag, index) => (\n        <span\n          key={`${tag}-${index}`}\n          data-slot=\"tag-input-tag\"\n          className=\"bg-muted text-foreground inline-flex items-center gap-1 rounded-full py-1 pr-1 pl-2.5 text-xs font-semibold\"\n        >\n          {tag}\n          <button\n            type=\"button\"\n            disabled={disabled}\n            aria-label={`Remove ${tag}`}\n            className=\"hover:bg-foreground/10 focus-visible:ring-ring inline-flex size-5 items-center justify-center rounded-full focus-visible:ring-2 focus-visible:outline-none\"\n            onClick={() => {\n              remove(index)\n              inputRef.current?.focus()\n            }}\n          >\n            <X aria-hidden=\"true\" size={12} />\n          </button>\n        </span>\n      ))}\n\n      <input\n        ref={inputRef}\n        data-slot=\"tag-input-field\"\n        aria-labelledby={`${reactId}-label`}\n        aria-describedby={`${reactId}-status`}\n        disabled={disabled || full}\n        placeholder={full ? `${max} is the limit` : placeholder}\n        value={draft}\n        className=\"placeholder:text-muted-foreground min-w-32 flex-1 bg-transparent px-1.5 py-1 text-sm outline-none\"\n        onChange={(event) => setDraft(event.target.value)}\n        onBlur={() => add(draft)}\n        onKeyDown={(event) => {\n          if (event.key === \"Enter\" || separators.includes(event.key)) {\n            event.preventDefault()\n            add(draft)\n            return\n          }\n\n          if (event.key === \"Backspace\" && draft === \"\" && tags.length > 0) {\n            event.preventDefault()\n            remove(tags.length - 1)\n          }\n        }}\n      />\n\n      <span\n        className=\"sr-only\"\n        id={`${reactId}-status`}\n        role=\"status\"\n        aria-live=\"polite\"\n      >\n        {message}\n      </span>\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "form",
    "input",
    "tags",
    "control"
  ],
  "type": "registry:ui"
}