{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tag-input",
  "title": "Tag Input",
  "description": "Type tags into one field, with suggestions, paste and keyboard removal.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.cuelark.com/r/theme.json",
    "https://ui.cuelark.com/r/motion.json"
  ],
  "files": [
    {
      "path": "src/registry/cuelark/ui/tag-input.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { XIcon } from \"lucide-react\";\n\nimport { gsap, motionScale } from \"@/lib/motion\";\nimport { cn } from \"@/lib/utils\";\n\ntype TagInputProps = Omit<React.ComponentProps<\"input\">, \"value\" | \"defaultValue\" | \"onChange\"> & {\n  value?: string[];\n  defaultValue?: string[];\n  onValueChange?: (tags: string[]) => void;\n  /** Suggestions shown while typing; Tab or Enter accepts the first match. */\n  suggestions?: string[];\n  max?: number;\n};\n\n/**\n * Tags typed into one field: Enter or comma adds, Backspace on an empty field selects then removes the last\n * tag, paste splits on commas. New tags pop in, duplicates shake instead of being added twice.\n */\nfunction TagInput({ value, defaultValue = [], onValueChange, suggestions = [], max, placeholder = \"Add a tag…\", className, ...props }: TagInputProps) {\n  const [internal, setInternal] = React.useState(defaultValue);\n  const tags = value ?? internal;\n  const [draft, setDraft] = React.useState(\"\");\n  const [armed, setArmed] = React.useState(false);\n  const listRef = React.useRef<HTMLDivElement>(null);\n  const match = draft ? suggestions.find((s) => s.toLowerCase().startsWith(draft.toLowerCase()) && !tags.includes(s)) : undefined;\n\n  const commit = (next: string[]) => {\n    setInternal(next);\n    onValueChange?.(next);\n  };\n  const add = (raw: string) => {\n    const items = raw.split(\",\").map((t) => t.trim()).filter(Boolean);\n    const next = [...tags];\n    for (const t of items) {\n      const dup = next.findIndex((x) => x.toLowerCase() === t.toLowerCase());\n      if (dup >= 0) {\n        const el = listRef.current?.children[dup];\n        if (el && motionScale(el)) gsap.fromTo(el, { x: 0 }, { keyframes: { x: [0, -4, 4, -2, 0] }, duration: 0.35 });\n      } else if (!max || next.length < max) next.push(t);\n    }\n    if (next.length !== tags.length) commit(next);\n    setDraft(\"\");\n  };\n\n  React.useLayoutEffect(() => {\n    const last = listRef.current?.lastElementChild;\n    if (last && motionScale(last) && last.getAttribute(\"data-new\") === \"true\") {\n      last.setAttribute(\"data-new\", \"false\");\n      gsap.fromTo(last, { scale: 0.8, opacity: 0 }, { scale: 1, opacity: 1, duration: 0.3, ease: \"cue-snap\" });\n    }\n  }, [tags.length]);\n\n  return (\n    <div\n      data-slot=\"tag-input\"\n      className={cn(\n        \"flex min-h-ctl-md w-full cursor-text flex-wrap items-center gap-1.5 rounded-md border border-input bg-card px-1.5 py-1 shadow-well transition-[border-color,box-shadow] duration-150 has-[input:focus-visible]:border-ring has-[input:focus-visible]:ring-3 has-[input:focus-visible]:ring-ring/20\",\n        className,\n      )}\n      onClick={(e) => e.currentTarget.querySelector(\"input\")?.focus()}\n    >\n      <div ref={listRef} className=\"contents\">\n        {tags.map((t, i) => (\n          <span\n            key={t}\n            data-new=\"true\"\n            className={cn(\n              \"inline-flex h-6 items-center gap-1 rounded-md border border-border bg-secondary ps-2 pe-1 text-xs font-medium transition-colors\",\n              armed && i === tags.length - 1 && \"border-destructive/50 bg-destructive/10 text-destructive\",\n            )}\n          >\n            {t}\n            <button\n              type=\"button\"\n              aria-label={`Remove ${t}`}\n              onClick={() => commit(tags.filter((x) => x !== t))}\n              className=\"relative after:absolute after:-inset-2 grid size-4 place-items-center rounded-sm text-muted-foreground outline-none hover:bg-foreground/10 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50\"\n            >\n              <XIcon className=\"size-3\" />\n            </button>\n          </span>\n        ))}\n      </div>\n      <div className=\"relative min-w-24 flex-1\">\n        <input\n          value={draft}\n          placeholder={tags.length ? \"\" : placeholder}\n          onChange={(e) => {\n            setArmed(false);\n            if (e.target.value.endsWith(\",\")) add(e.target.value);\n            else setDraft(e.target.value);\n          }}\n          onPaste={(e) => {\n            const text = e.clipboardData.getData(\"text\");\n            if (text.includes(\",\")) {\n              e.preventDefault();\n              add(text);\n            }\n          }}\n          onKeyDown={(e) => {\n            if (e.key === \"Enter\" && draft) {\n              e.preventDefault();\n              add(draft);\n            } else if (e.key === \"Tab\" && match) {\n              e.preventDefault();\n              add(match);\n            } else if (e.key === \"Backspace\" && !draft && tags.length) {\n              if (armed) {\n                commit(tags.slice(0, -1));\n                setArmed(false);\n              } else setArmed(true);\n            } else if (e.key === \"Escape\") setArmed(false);\n          }}\n          className=\"h-6 w-full bg-transparent px-1 text-sm outline-none placeholder:text-muted-foreground/80\"\n          {...props}\n        />\n        {match && (\n          <span aria-hidden=\"true\" className=\"pointer-events-none absolute inset-y-0 left-1 flex items-center text-sm text-muted-foreground/60\">\n            <span className=\"invisible\">{draft}</span>\n            {match.slice(draft.length)}\n            <kbd className=\"cue-kbd ms-2 h-4 text-[0.625rem]\">tab</kbd>\n          </span>\n        )}\n      </div>\n    </div>\n  );\n}\n\nexport { TagInput };\nexport type { TagInputProps };\n",
      "type": "registry:ui"
    }
  ],
  "docs": "Docs and examples: https://ui.cuelark.com/components/tag-input",
  "categories": [
    "forms"
  ],
  "type": "registry:ui"
}