{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "tags-input",
  "title": "Tags Input",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/tags-input/tags-input.component.ts",
      "content": "import {\n  Component,\n  ElementRef,\n  EventEmitter,\n  Input,\n  Output,\n  ViewChild,\n  booleanAttribute,\n  signal,\n  ChangeDetectionStrategy,\n} from '@angular/core'\nimport { cn } from '@/lib/utils'\n\n/**\n * Angular port of UIPKGE TagsInput, 1:1 with the React component: type a value and press a\n * commit key (Enter / comma by default, or `addOnKeys` / `delimiter`) to add a chip;\n * Backspace on an empty draft removes the last chip; blur commits the draft; `addOnPaste`\n * splits pasted text on whitespace; `unique` rejects duplicates and `max` caps the list.\n * Clicking anywhere in the box focuses the input. `value` / `defaultValue` / `valueChange`\n * work controlled or uncontrolled like React. Class strings copied verbatim from React.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-tags-input, [ui-tags-input]',\n  standalone: true,\n  host: {\n    'data-uipkge': '',\n    'data-slot': 'tags-input',\n    '[class]': 'hostClass',\n    '(click)': 'focusInput()',\n  },\n  template: `\n    @for (tag of tags; track $index; let i = $index) {\n      <span\n        data-uipkge=\"\"\n        data-slot=\"tags-input-item\"\n        class=\"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\" class=\"rounded bg-transparent px-2 py-0.5 text-sm\">{{ tag }}</span>\n        <button\n          type=\"button\"\n          [attr.aria-label]=\"'Remove ' + tag\"\n          [disabled]=\"disabled\"\n          data-slot=\"tags-input-item-delete\"\n          class=\"hover:text-foreground focus-visible:ring-ring mr-1 flex rounded bg-transparent focus-visible:ring-2 focus-visible:outline-none disabled:pointer-events-none\"\n          (mousedown)=\"$event.preventDefault()\"\n          (click)=\"$event.stopPropagation(); removeAt(i)\"\n        >\n          <svg\n            xmlns=\"http://www.w3.org/2000/svg\"\n            width=\"24\"\n            height=\"24\"\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            stroke-width=\"2\"\n            stroke-linecap=\"round\"\n            stroke-linejoin=\"round\"\n            class=\"lucide lucide-x h-4 w-4\"\n            aria-hidden=\"true\"\n          >\n            <path d=\"M18 6 6 18\" />\n            <path d=\"m6 6 12 12\" />\n          </svg>\n        </button>\n      </span>\n    }\n\n    @if (!disabled) {\n      <input\n        #input\n        data-slot=\"tags-input-input\"\n        [value]=\"draft()\"\n        [attr.placeholder]=\"atMax ? null : (placeholder ?? null)\"\n        [disabled]=\"atMax\"\n        class=\"min-h-5 flex-1 bg-transparent px-1 text-sm focus:outline-none disabled:cursor-default\"\n        (input)=\"draft.set($any($event.target).value)\"\n        (keydown)=\"handleKeyDown($event)\"\n        (paste)=\"handlePaste($event)\"\n        (blur)=\"addTag(draft())\"\n      />\n    }\n  `,\n})\nexport class UiTagsInputComponent {\n  /** Controlled list of tags (pair with `valueChange`). Leave unset for uncontrolled use. */\n  @Input() value?: string[]\n  /** Uncontrolled initial list. */\n  @Input() defaultValue?: string[]\n  @Input() placeholder?: string\n  @Input({ transform: booleanAttribute }) disabled = false\n  /** Keys that commit the typed value into a tag. Defaults to Enter + comma. */\n  @Input() addOnKeys?: string[]\n  /** Split pasted text on whitespace and add each token as a tag. */\n  @Input({ transform: booleanAttribute }) addOnPaste = false\n  /** Character that commits the typed value into a tag (alias for addOnKeys). */\n  @Input() delimiter?: string\n  /** Reject duplicate tags (case-sensitive). */\n  @Input({ transform: booleanAttribute }) unique = true\n  /** Maximum number of tags allowed. */\n  @Input() max?: number\n  @Input('class') className?: string\n\n  /** Fires with the next list whenever a tag is added or removed (React `onValueChange`). */\n  @Output() valueChange = new EventEmitter<string[]>()\n\n  @ViewChild('input') private inputRef?: ElementRef<HTMLInputElement>\n\n  readonly draft = signal('')\n  private readonly internal = signal<string[] | null>(null)\n\n  get tags(): string[] {\n    if (this.value !== undefined) return this.value\n    return this.internal() ?? this.defaultValue ?? []\n  }\n\n  get commitKeys(): string[] {\n    return this.addOnKeys ?? (this.delimiter ? [this.delimiter] : ['Enter', ','])\n  }\n\n  get atMax(): boolean {\n    return this.max !== undefined && this.tags.length >= this.max\n  }\n\n  get hostClass(): string {\n    return 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      this.disabled && 'pointer-events-none opacity-50',\n      this.className,\n    )\n  }\n\n  focusInput(): void {\n    if (!this.disabled) this.inputRef?.nativeElement.focus()\n  }\n\n  private commit(next: string[]): void {\n    const capped = this.max !== undefined ? next.slice(0, this.max) : next\n    if (this.value === undefined) this.internal.set(capped)\n    this.valueChange.emit(capped)\n  }\n\n  addTag(raw: string): void {\n    const trimmed = raw.trim()\n    if (!trimmed) return\n    if (this.atMax || (this.unique && this.tags.includes(trimmed))) {\n      this.setDraft('')\n      return\n    }\n    this.commit([...this.tags, trimmed])\n    this.setDraft('')\n  }\n\n  removeAt(index: number): void {\n    this.commit(this.tags.filter((_, i) => i !== index))\n  }\n\n  handleKeyDown(e: KeyboardEvent): void {\n    if (this.commitKeys.includes(e.key)) {\n      e.preventDefault()\n      this.addTag(this.draft())\n    } else if (e.key === 'Backspace' && this.draft() === '' && this.tags.length > 0) {\n      this.removeAt(this.tags.length - 1)\n    }\n  }\n\n  handlePaste(e: ClipboardEvent): void {\n    if (!this.addOnPaste) return\n    const text = e.clipboardData?.getData('text') ?? ''\n    if (!text.trim()) return\n    e.preventDefault()\n    const tokens = text\n      .split(/\\s+/)\n      .map((t) => t.trim())\n      .filter(Boolean)\n    let next = [...this.tags]\n    for (const token of tokens) {\n      if (this.max !== undefined && next.length >= this.max) break\n      if (this.unique && next.includes(token)) continue\n      next = [...next, token]\n    }\n    this.commit(next)\n    this.setDraft('')\n  }\n\n  /** The input is bound to `draft`, but also reset its DOM value (a signal set to '' twice is a no-op). */\n  private setDraft(v: string): void {\n    this.draft.set(v)\n    if (this.inputRef) this.inputRef.nativeElement.value = v\n  }\n}\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/tags-input/tags-input.component.ts"
    },
    {
      "path": "packages/registry-angular/components/tags-input/index.ts",
      "content": "export { UiTagsInputComponent } from './tags-input.component'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/tags-input/index.ts"
    }
  ],
  "dependencies": [],
  "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"
  ]
}