{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tags-input",
  "title": "Tags Input",
  "description": "Type, paste or remove tags with full keyboard support.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "badge"
  ],
  "files": [
    {
      "path": "registry/default/ui/tags-input.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { XIcon } from \"lucide-react\"\n\nimport { cn } from \"@/registry/default/lib/utils\"\nimport { Badge } from \"@/registry/default/ui/badge\"\n\nconst PASTE_SEPARATORS = /[,\\n\\t]/\n\nfunction TagsInput({\n  value,\n  defaultValue = [],\n  onValueChange,\n  max,\n  allowDuplicates = false,\n  validate,\n  addOnBlur = false,\n  disabled,\n  placeholder,\n  className,\n  onKeyDown,\n  onBlur,\n  onPaste,\n  ...props\n}: Omit<\n  React.ComponentProps<\"input\">,\n  \"value\" | \"defaultValue\" | \"onChange\" | \"type\" | \"max\"\n> & {\n  value?: string[]\n  defaultValue?: string[]\n  onValueChange?: (tags: string[]) => void\n  max?: number\n  allowDuplicates?: boolean\n  validate?: (tag: string) => boolean\n  addOnBlur?: boolean\n}) {\n  const inputRef = React.useRef<HTMLInputElement>(null)\n  const [uncontrolledTags, setUncontrolledTags] = React.useState(defaultValue)\n  const [draft, setDraft] = React.useState(\"\")\n  const [activeIndex, setActiveIndex] = React.useState<number | null>(null)\n  const [announcement, setAnnouncement] = React.useState(\"\")\n  const tags = value ?? uncontrolledTags\n  const isFull = max !== undefined && tags.length >= max\n\n  function updateTags(next: string[]) {\n    if (value === undefined) setUncontrolledTags(next)\n    onValueChange?.(next)\n  }\n\n  function addTags(candidates: string[]) {\n    const accepted: string[] = []\n    for (const candidate of candidates) {\n      const tag = candidate.trim()\n      const isDuplicate = [...tags, ...accepted].includes(tag)\n      const hasRoom = max === undefined || tags.length + accepted.length < max\n      if (!tag || !hasRoom) continue\n      if (isDuplicate && !allowDuplicates) continue\n      if (validate && !validate(tag)) continue\n      accepted.push(tag)\n    }\n    if (accepted.length === 0) return false\n\n    const next = [...tags, ...accepted]\n    updateTags(next)\n    const limitNote =\n      max !== undefined && next.length >= max\n        ? `. Limit of ${max} reached, remove a tag to add more`\n        : \"\"\n    setAnnouncement(`Added ${accepted.join(\", \")}${limitNote}`)\n    return true\n  }\n\n  function selectTag(index: number | null) {\n    setActiveIndex(index)\n    if (index === null) return\n    setAnnouncement(\n      `${tags[index]} selected, ${index + 1} of ${tags.length}. Press Backspace to remove`\n    )\n  }\n\n  function removeTag(index: number) {\n    const removed = tags[index]\n    updateTags(tags.filter((_, tagIndex) => tagIndex !== index))\n    setActiveIndex(null)\n    setAnnouncement(`Removed ${removed}`)\n    inputRef.current?.focus()\n  }\n\n  function commitDraft() {\n    if (addTags([draft])) setDraft(\"\")\n  }\n\n  function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {\n    onKeyDown?.(event)\n    if (event.defaultPrevented) return\n\n    const { selectionStart, selectionEnd } = event.currentTarget\n    const isCaretAtStart = selectionStart === 0 && selectionEnd === 0\n    const lastIndex = tags.length - 1\n\n    switch (event.key) {\n      case \"Enter\":\n      case \",\":\n        event.preventDefault()\n        commitDraft()\n        return\n      case \"Backspace\":\n        if (!isCaretAtStart || tags.length === 0) return\n        event.preventDefault()\n        if (activeIndex === null) selectTag(lastIndex)\n        else removeTag(activeIndex)\n        return\n      case \"Delete\":\n        if (activeIndex === null) return\n        event.preventDefault()\n        removeTag(activeIndex)\n        return\n      case \"ArrowLeft\":\n        if (!isCaretAtStart || tags.length === 0) return\n        event.preventDefault()\n        selectTag(\n          activeIndex === null ? lastIndex : Math.max(0, activeIndex - 1)\n        )\n        return\n      case \"ArrowRight\":\n        if (activeIndex === null) return\n        event.preventDefault()\n        selectTag(activeIndex >= lastIndex ? null : activeIndex + 1)\n        return\n      case \"Escape\":\n        setActiveIndex(null)\n        return\n      default:\n        setActiveIndex(null)\n    }\n  }\n\n  return (\n    <div\n      data-slot=\"tags-input\"\n      data-disabled={disabled ? \"\" : undefined}\n      onClick={() => inputRef.current?.focus()}\n      className={cn(\n        \"flex min-h-9 w-full min-w-0 cursor-text flex-wrap items-center gap-1.5 rounded-md border border-input bg-transparent px-2 py-1.5 text-base shadow-xs transition-[color,box-shadow] md:text-sm dark:bg-input/30\",\n        \"focus-within:border-ring focus-within:ring-[3px] focus-within:ring-ring/50\",\n        \"has-[input[aria-invalid=true]]:border-destructive has-[input[aria-invalid=true]]:ring-destructive/20\",\n        \"data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n        className\n      )}\n    >\n      {tags.map((tag, index) => (\n        <Badge\n          key={`${tag}-${tags.slice(0, index).filter((t) => t === tag).length}`}\n          data-slot=\"tags-input-tag\"\n          data-active={activeIndex === index ? \"\" : undefined}\n          variant=\"secondary\"\n          className=\"gap-1 pr-1 data-[active]:ring-2 data-[active]:ring-ring\"\n        >\n          {tag}\n          <button\n            type=\"button\"\n            tabIndex={-1}\n            aria-label={`Remove ${tag}`}\n            disabled={disabled}\n            onClick={(event) => {\n              event.stopPropagation()\n              removeTag(index)\n            }}\n            className=\"rounded-sm text-muted-foreground transition-colors hover:text-foreground\"\n          >\n            <XIcon className=\"size-3\" />\n          </button>\n        </Badge>\n      ))}\n      <input\n        ref={inputRef}\n        type=\"text\"\n        data-slot=\"tags-input-field\"\n        value={draft}\n        disabled={disabled}\n        readOnly={isFull}\n        placeholder={isFull ? undefined : placeholder}\n        onChange={(event) => setDraft(event.target.value)}\n        onKeyDown={handleKeyDown}\n        onPaste={(event) => {\n          onPaste?.(event)\n          const text = event.clipboardData.getData(\"text\")\n          if (!PASTE_SEPARATORS.test(text)) return\n          event.preventDefault()\n          addTags(text.split(PASTE_SEPARATORS))\n        }}\n        onBlur={(event) => {\n          onBlur?.(event)\n          setActiveIndex(null)\n          if (addOnBlur) commitDraft()\n        }}\n        className=\"h-6 min-w-20 flex-1 bg-transparent px-1 outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed\"\n        {...props}\n      />\n      <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n        {announcement}\n      </span>\n    </div>\n  )\n}\n\nexport { TagsInput }\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}