{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tags-input",
  "title": "Tags Input",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-react/components/tags-input/tags-input.tsx",
      "content": "'use client'\n\nimport * as React from 'react'\nimport { X } from 'lucide-react'\nimport { cn } from '@/lib/utils'\n\nexport interface TagsInputProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {\n  /** Controlled list of tags. */\n  value?: string[]\n  /** Fires with the next list whenever a tag is added or removed. */\n  onValueChange?: (value: string[]) => void\n  /** Uncontrolled initial list. */\n  defaultValue?: string[]\n  placeholder?: string\n  disabled?: boolean\n  /** Characters that commit the typed value into a tag. Defaults to Enter + comma. */\n  addOnKeys?: string[]\n  /** Split pasted text on whitespace and add each token as a tag. */\n  addOnPaste?: boolean\n  /** Character that commits the typed value into a tag (alias for addOnKeys). */\n  delimiter?: string\n  /** Reject duplicate tags (case-sensitive). Defaults to true. */\n  unique?: boolean\n}\n\nconst TagsInput = React.forwardRef<HTMLDivElement, TagsInputProps>(\n  (\n    {\n      className,\n      value,\n      onValueChange,\n      defaultValue,\n      placeholder,\n      disabled,\n      addOnKeys,\n      addOnPaste = false,\n      delimiter,\n      unique = true,\n      ...props\n    },\n    ref,\n  ) => {\n    const commitKeys = addOnKeys ?? (delimiter ? [delimiter] : ['Enter', ','])\n    const isControlled = value !== undefined\n    const [internal, setInternal] = React.useState<string[]>(defaultValue ?? [])\n    const tags = isControlled ? (value as string[]) : internal\n    const [draft, setDraft] = React.useState('')\n    const inputRef = React.useRef<HTMLInputElement | null>(null)\n\n    function commit(next: string[]) {\n      if (!isControlled) setInternal(next)\n      onValueChange?.(next)\n    }\n\n    function addTag(raw: string) {\n      const trimmed = raw.trim()\n      if (!trimmed) return\n      if (unique && tags.includes(trimmed)) {\n        setDraft('')\n        return\n      }\n      commit([...tags, trimmed])\n      setDraft('')\n    }\n\n    function removeAt(index: number) {\n      commit(tags.filter((_, i) => i !== index))\n    }\n\n    function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {\n      if (commitKeys.includes(e.key)) {\n        e.preventDefault()\n        addTag(draft)\n      } else if (e.key === 'Backspace' && draft === '' && tags.length > 0) {\n        removeAt(tags.length - 1)\n      }\n    }\n\n    function handlePaste(e: React.ClipboardEvent<HTMLInputElement>) {\n      if (!addOnPaste) return\n      const text = e.clipboardData.getData('text')\n      if (!text.trim()) return\n      e.preventDefault()\n      const tokens = text.split(/\\s+/).map((t) => t.trim()).filter(Boolean)\n      let next = [...tags]\n      for (const token of tokens) {\n        if (unique && next.includes(token)) continue\n        next = [...next, token]\n      }\n      commit(next)\n      setDraft('')\n    }\n\n    return (\n      <div\n        ref={ref}\n        data-uipkge=\"\"\n        data-slot=\"tags-input\"\n        className={cn(\n          'border-input bg-background flex flex-wrap items-center gap-2 rounded-md border px-2 py-1 text-sm shadow-xs transition-[color,box-shadow] outline-none',\n          'focus-within:border-ring focus-within:ring-ring/50 focus-within:ring-[3px]',\n          'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',\n          className,\n        )}\n        onClick={() => inputRef.current?.focus()}\n        {...props}\n      >\n        {tags.map((tag, i) => (\n          <span\n            key={`${tag}-${i}`}\n            data-uipkge=\"\"\n            data-slot=\"tags-input-item\"\n            className=\"bg-secondary data-[state=active]:ring-ring ring-offset-background flex h-5 items-center rounded-md data-[state=active]:ring-2 data-[state=active]:ring-offset-2\"\n          >\n            <span data-slot=\"tags-input-item-text\" className=\"rounded bg-transparent px-2 py-0.5 text-sm\">\n              {tag}\n            </span>\n            <button\n              type=\"button\"\n              aria-label={`Remove ${tag}`}\n              disabled={disabled}\n              data-slot=\"tags-input-item-delete\"\n              className=\"mr-1 flex rounded bg-transparent\"\n              onMouseDown={(e) => e.preventDefault()}\n              onClick={(e) => {\n                e.stopPropagation()\n                removeAt(i)\n              }}\n            >\n              <X className=\"h-4 w-4\" aria-hidden=\"true\" />\n            </button>\n          </span>\n        ))}\n\n        <input\n          ref={inputRef}\n          data-slot=\"tags-input-input\"\n          value={draft}\n          placeholder={placeholder}\n          disabled={disabled}\n          className=\"min-h-5 flex-1 bg-transparent px-1 text-sm focus:outline-none\"\n          onChange={(e) => setDraft(e.target.value)}\n          onKeyDown={handleKeyDown}\n          onPaste={handlePaste}\n          onBlur={() => addTag(draft)}\n        />\n      </div>\n    )\n  },\n)\nTagsInput.displayName = 'TagsInput'\n\nexport { TagsInput }\n",
      "type": "registry:ui",
      "target": "~/components/ui/tags-input/tags-input.tsx"
    },
    {
      "path": "packages/registry-react/components/tags-input/index.ts",
      "content": "export { TagsInput, type TagsInputProps } from './tags-input'\n",
      "type": "registry:ui",
      "target": "~/components/ui/tags-input/index.ts"
    }
  ],
  "dependencies": [
    "lucide-react"
  ],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Multi-tag input — type a value, hit Enter, get a Chip. Backspace removes the last tag. Use for email recipient lists, tag sets, and free-form keyword inputs.",
  "categories": [
    "form"
  ]
}