{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "multi-select",
  "type": "registry:ui",
  "title": "MultiSelect",
  "description": "Scoped multi-select tag picker with badge pills, search filter, and batch actions.",
  "registryDependencies": [
    "https://ui.arpitbhayani.me/r/aui.json"
  ],
  "files": [
    {
      "path": "ui/multi-select.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface MultiSelectOption {\n  value: string;\n  label: string;\n  group?: string;\n  description?: string;\n}\n\nexport interface MultiSelectProps {\n  options: MultiSelectOption[];\n  selected: string[];\n  onChange: (selected: string[]) => void;\n  placeholder?: string;\n  label?: string;\n  error?: string;\n  maxDisplayedBadges?: number;\n  className?: string;\n}\n\nexport function MultiSelect({\n  options = [],\n  selected = [],\n  onChange,\n  placeholder = \"Select options...\",\n  label,\n  error,\n  maxDisplayedBadges = 3,\n  className,\n}: MultiSelectProps) {\n  const [isOpen, setIsOpen] = React.useState(false);\n  const [search, setSearch] = React.useState(\"\");\n\n  const containerRef = React.useRef<HTMLDivElement>(null);\n  const inputRef = React.useRef<HTMLInputElement>(null);\n\n  React.useEffect(() => {\n    if (!isOpen) return;\n    const handleClickOutside = (e: MouseEvent) => {\n      if (\n        containerRef.current &&\n        !containerRef.current.contains(e.target as Node)\n      ) {\n        setIsOpen(false);\n      }\n    };\n    document.addEventListener(\"mousedown\", handleClickOutside);\n    return () => document.removeEventListener(\"mousedown\", handleClickOutside);\n  }, [isOpen]);\n\n  const filteredOptions = React.useMemo(() => {\n    if (!search.trim()) return options;\n    const q = search.toLowerCase();\n    return options.filter(\n      (opt) =>\n        opt.label.toLowerCase().includes(q) ||\n        (opt.description && opt.description.toLowerCase().includes(q))\n    );\n  }, [options, search]);\n\n  const toggleOption = (val: string) => {\n    if (selected.includes(val)) {\n      onChange(selected.filter((item) => item !== val));\n    } else {\n      onChange([...selected, val]);\n    }\n  };\n\n  const removeBadge = (e: React.MouseEvent, val: string) => {\n    e.stopPropagation();\n    onChange(selected.filter((item) => item !== val));\n  };\n\n  const visibleSelected = selected.slice(0, maxDisplayedBadges);\n  const remainingCount = selected.length - maxDisplayedBadges;\n\n  return (\n    <div className={cn(\"flex flex-col gap-1.5 w-full\", className)} ref={containerRef}>\n      {label && <label className=\"text-xs font-medium text-foreground\">{label}</label>}\n\n      <div\n        className={cn(\n          \"relative min-h-[38px] w-full flex items-center justify-between rounded-md border border-input bg-background p-1.5 text-sm shadow-xs cursor-pointer transition-colors\",\n          isOpen && \"ring-1 ring-ring border-primary\",\n          error && \"border-destructive\"\n        )}\n        onClick={() => setIsOpen(!isOpen)}\n      >\n        <div className=\"flex flex-wrap gap-1 flex-1 items-center\">\n          {selected.length === 0 ? (\n            <span className=\"text-muted-foreground px-1\">{placeholder}</span>\n          ) : (\n            <>\n              {visibleSelected.map((val) => {\n                const opt = options.find((o) => o.value === val);\n                const itemLabel = opt ? opt.label : val;\n                return (\n                  <span\n                    key={val}\n                    className=\"inline-flex items-center gap-1 font-mono text-xs px-2 py-0.5 rounded border border-border bg-muted text-foreground\"\n                  >\n                    <span>{itemLabel}</span>\n                    <button\n                      type=\"button\"\n                      className=\"text-muted-foreground hover:text-foreground leading-none\"\n                      onClick={(e) => removeBadge(e, val)}\n                    >\n                      ×\n                    </button>\n                  </span>\n                );\n              })}\n              {remainingCount > 0 && (\n                <span className=\"font-mono text-xs px-1.5 py-0.5 rounded border border-border text-muted-foreground\">\n                  +{remainingCount}\n                </span>\n              )}\n            </>\n          )}\n        </div>\n\n        <span className=\"text-muted-foreground px-1\">▾</span>\n      </div>\n\n      {isOpen && (\n        <div className=\"relative\">\n          <div className=\"absolute top-1 left-0 right-0 z-50 rounded-md border border-border bg-popover text-popover-foreground shadow-lg overflow-hidden animate-in fade-in-0\">\n            <div className=\"p-2 border-b border-border bg-muted/30\">\n              <input\n                ref={inputRef}\n                type=\"text\"\n                className=\"w-full bg-transparent text-sm outline-hidden placeholder:text-muted-foreground\"\n                placeholder=\"Filter tags...\"\n                value={search}\n                onChange={(e) => setSearch(e.target.value)}\n                onClick={(e) => e.stopPropagation()}\n              />\n            </div>\n\n            <div className=\"flex justify-end gap-2 p-1 border-b border-border bg-muted/10 font-mono text-xs\">\n              <button\n                type=\"button\"\n                className=\"px-2 py-0.5 text-muted-foreground hover:text-primary\"\n                onClick={(e) => {\n                  e.stopPropagation();\n                  onChange(Array.from(new Set([...selected, ...filteredOptions.map((o) => o.value)])));\n                }}\n              >\n                Select All\n              </button>\n              <button\n                type=\"button\"\n                className=\"px-2 py-0.5 text-muted-foreground hover:text-primary\"\n                onClick={(e) => {\n                  e.stopPropagation();\n                  onChange([]);\n                }}\n              >\n                Clear All\n              </button>\n            </div>\n\n            <ul className=\"max-h-[220px] overflow-y-auto p-1 text-sm\">\n              {filteredOptions.length === 0 ? (\n                <li className=\"p-4 text-center text-xs text-muted-foreground\">No options found</li>\n              ) : (\n                filteredOptions.map((opt) => {\n                  const isChecked = selected.includes(opt.value);\n                  return (\n                    <li\n                      key={opt.value}\n                      className={cn(\n                        \"flex items-center gap-2 p-2 rounded cursor-pointer hover:bg-accent\",\n                        isChecked && \"font-medium\"\n                      )}\n                      onClick={(e) => {\n                        e.stopPropagation();\n                        toggleOption(opt.value);\n                      }}\n                    >\n                      <input\n                        type=\"checkbox\"\n                        checked={isChecked}\n                        onChange={() => {}}\n                        className=\"h-3.5 w-3.5 accent-primary\"\n                      />\n                      <span className=\"flex-1\">{opt.label}</span>\n                      {opt.group && (\n                        <span className=\"font-mono text-xs text-muted-foreground bg-muted px-1.5 py-0.5 rounded\">\n                          {opt.group}\n                        </span>\n                      )}\n                    </li>\n                  );\n                })\n              )}\n            </ul>\n          </div>\n        </div>\n      )}\n\n      {error && <p className=\"text-xs text-destructive\">{error}</p>}\n    </div>\n  );\n}\n\nexport const TagInput = MultiSelect;\n",
      "type": "registry:ui"
    }
  ]
}