{"$schema":"https://ui.logic2b.com/schema/registry-item.json","name":"tags-input","type":"registry:ui","description":"Free-form multi-value input rendered as removable chips: Enter or comma commits, paste splits on commas, Backspace on an empty field removes the last tag, duplicates are ignored and max caps the list. Controlled or uncontrolled via value/defaultValue + onValueChange.","dependencies":["lucide-react"],"registryDependencies":["utils"],"files":[{"path":"ui/tags-input.tsx","type":"registry:ui","content":"\"use client\"\n\nimport * as React from \"react\"\nimport { XIcon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/**\n * Tags input — free-form multi-value entry rendered as removable chips.\n * Enter or comma commits the draft, paste splits on commas, Backspace on an\n * empty draft removes the last tag. Duplicates are ignored; `max` caps the\n * list.\n */\n\nfunction TagsInput({\n  className,\n  value: valueProp,\n  defaultValue,\n  onValueChange,\n  placeholder,\n  max,\n  disabled,\n  id,\n  \"aria-label\": ariaLabel,\n  \"aria-labelledby\": ariaLabelledby,\n  ...props\n}: Omit<React.ComponentProps<\"div\">, \"defaultValue\" | \"onChange\"> & {\n  value?: string[]\n  defaultValue?: string[]\n  onValueChange?: (value: string[]) => void\n  placeholder?: string\n  max?: number\n  disabled?: boolean\n}) {\n  const inputRef = React.useRef<HTMLInputElement>(null)\n  const [draft, setDraft] = React.useState(\"\")\n  const [internal, setInternal] = React.useState(defaultValue ?? [])\n  const isControlled = valueProp !== undefined\n  const value = isControlled ? valueProp : internal\n  const setValue = (next: string[]) => {\n    if (!isControlled) setInternal(next)\n    onValueChange?.(next)\n  }\n\n  const atMax = max !== undefined && value.length >= max\n\n  const commit = (raw: string) => {\n    const parts = raw\n      .split(\",\")\n      .map((t) => t.trim())\n      .filter(Boolean)\n    if (parts.length === 0) return\n    const tags = parts.filter((t) => !value.includes(t))\n    if (tags.length > 0) {\n      const next = [...value, ...tags]\n      setValue(max !== undefined ? next.slice(0, max) : next)\n    }\n    // Consume the draft even when everything was a duplicate.\n    setDraft(\"\")\n  }\n\n  const remove = (tag: string) => setValue(value.filter((t) => t !== tag))\n\n  return (\n    <div\n      data-slot=\"tags-input\"\n      data-disabled={disabled || undefined}\n      className={cn(\n        \"border-input dark:bg-input/30 focus-within:border-ring focus-within:ring-ring/50 flex min-h-9 w-full flex-wrap items-center gap-1 rounded-md border bg-transparent px-2 py-1 text-sm shadow-xs transition-[color,box-shadow] focus-within:ring-[3px]\",\n        disabled && \"pointer-events-none cursor-not-allowed opacity-50\",\n        className\n      )}\n      onClick={() => inputRef.current?.focus()}\n      {...props}\n    >\n      {value.map((tag) => (\n        <span\n          key={tag}\n          data-slot=\"tags-input-tag\"\n          className=\"bg-secondary text-secondary-foreground inline-flex max-w-full items-center gap-1 rounded-sm px-1.5 py-0.5 text-xs font-medium\"\n        >\n          <span className=\"truncate\">{tag}</span>\n          <button\n            type=\"button\"\n            data-slot=\"tags-input-remove\"\n            aria-label={`Remove ${tag}`}\n            tabIndex={-1}\n            disabled={disabled}\n            className=\"text-muted-foreground hover:text-foreground focus-visible:ring-ring/50 rounded-xs outline-none focus-visible:ring-2\"\n            onClick={(event) => {\n              event.stopPropagation()\n              remove(tag)\n            }}\n          >\n            <XIcon className=\"size-3\" />\n          </button>\n        </span>\n      ))}\n      <input\n        ref={inputRef}\n        id={id}\n        data-slot=\"tags-input-field\"\n        type=\"text\"\n        value={draft}\n        disabled={disabled || atMax}\n        placeholder={atMax ? undefined : placeholder}\n        aria-label={ariaLabel}\n        aria-labelledby={ariaLabelledby}\n        className=\"placeholder:text-muted-foreground min-w-16 flex-1 bg-transparent py-0.5 outline-none disabled:cursor-not-allowed\"\n        onChange={(event) => setDraft(event.target.value)}\n        onKeyDown={(event) => {\n          if (event.key === \"Enter\" || event.key === \",\") {\n            event.preventDefault()\n            commit(draft)\n          } else if (event.key === \"Backspace\" && draft === \"\" && value.length > 0) {\n            remove(value[value.length - 1])\n          }\n        }}\n        onPaste={(event) => {\n          const text = event.clipboardData.getData(\"text\")\n          if (text.includes(\",\")) {\n            event.preventDefault()\n            commit(draft + text)\n          }\n        }}\n        onBlur={() => commit(draft)}\n      />\n    </div>\n  )\n}\n\nexport { TagsInput }\n"}],"categories":["form"],"accessibility":{"support":"authored","pattern":"multi-value textbox","primitive":"HTMLInputElement","keyboard":[{"keys":["Enter",","],"action":"Commit the current text as a tag."},{"keys":["Backspace"],"action":"Remove the last tag when the text field is empty."}],"aria":["The editable input receives id/aria-label/aria-labelledby; removal buttons name their tag."],"consumer":["Provide a visible label and announce additions/removals in application context when that feedback is important."]},"api":{"source":"src/ui/tags-input.tsx","exports":[{"name":"TagsInput","kind":"component","description":"Tags input — free-form multi-value entry rendered as removable chips. Enter or comma commits the draft, paste splits on commas, Backspace on an empty draft removes the last tag. Duplicates are ignored; `max` caps the list.","propsType":"Omit<React.ComponentProps<\"div\">, \"defaultValue\" | \"onChange\"> & { value?: string[] defaultValue?: string[] onValueChange?: (value: string[]) => void placeholder?: string max?: number disabled?: boolean }","props":[{"name":"value","type":"string[]","required":false},{"name":"defaultValue","type":"string[]","required":false},{"name":"onValueChange","type":"(value: string[]) => void","required":false},{"name":"placeholder","type":"string","required":false},{"name":"max","type":"number","required":false},{"name":"disabled","type":"boolean","required":false}]}]}}