{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "extends": "none",
  "name": "tag-field",
  "title": "tag-field",
  "description": "tag-field",
  "dependencies": [
    "react-aria-components",
    "cn"
  ],
  "registryDependencies": [
    "https://intentui.com/r/field",
    "https://intentui.com/r/tag-group",
    "https://intentui.com/r/text-field"
  ],
  "files": [
    {
      "path": "src/components/ui/tag-field.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useMemo, useRef, useState } from \"react\"\nimport type { Key, Selection } from \"react-aria-components/TagGroup\"\nimport type { TextFieldProps } from \"react-aria-components/TextField\"\nimport { cn } from \"cn\"\nimport { FieldError } from \"@/components/ui/field\"\nimport { Tag, TagGroup, TagList } from \"@/components/ui/tag-group\"\nimport { TextField } from \"@/components/ui/text-field\"\n\nexport interface TagInputProps extends Pick<\n  TextFieldProps,\n  \"isDisabled\" | \"isReadOnly\" | \"children\" | \"aria-label\" | \"aria-labelledby\"\n> {\n  value?: Selection\n  onChange?: (next: Selection) => void\n  defaultValue?: string[]\n  splitPattern?: RegExp\n  className?: string\n  inputValue?: string\n  onInputValueChange?: (v: string) => void\n  isRequired?: boolean\n  requiredMessage?: string\n  name?: string\n}\n\nexport function TagField({\n  value,\n  onChange,\n  defaultValue = [],\n  splitPattern = /[,;]/,\n  className,\n  inputValue: controlledInput,\n  onInputValueChange,\n  isRequired,\n  requiredMessage,\n  name = \"tags\",\n  children,\n  ...props\n}: TagInputProps) {\n  const [internalSelection, setInternalSelection] = useState<Selection>(new Set(defaultValue))\n  const [uncontrolledInput, setUncontrolledInput] = useState(\"\")\n  const [touched, setTouched] = useState(false)\n  const hiddenRef = useRef<HTMLInputElement>(null)\n\n  const selection: Selection = value ?? internalSelection\n  const inputValue = controlledInput ?? uncontrolledInput\n  const setInputValue = onInputValueChange ?? setUncontrolledInput\n  const applySelection = (next: Selection) => (onChange ?? setInternalSelection)(next as Selection)\n\n  const list = useMemo(() => {\n    return selection === \"all\" ? [] : Array.from(selection).map((v) => String(v))\n  }, [selection])\n\n  const isInvalid = Boolean(isRequired && list.length === 0 && touched)\n  const errorText = requiredMessage ?? \"At least one item is required\"\n\n  useEffect(() => {\n    const input = hiddenRef.current\n    const form = input?.form\n    if (!form || !input) return\n    const onSubmit = (e: Event) => {\n      if (isRequired && list.length === 0) {\n        e.preventDefault()\n        setTouched(true)\n        input.setCustomValidity(errorText)\n        form.reportValidity()\n      } else {\n        input.setCustomValidity(\"\")\n      }\n    }\n    form.addEventListener(\"submit\", onSubmit)\n    return () => form.removeEventListener(\"submit\", onSubmit)\n  }, [isRequired, list.length, errorText])\n\n  function handleKeyDown(e: React.KeyboardEvent) {\n    if (e.key === \"Enter\" || e.key === \",\" || e.key === \";\") {\n      e.preventDefault()\n      addTag()\n    }\n  }\n\n  function addTag() {\n    if (selection === \"all\") return\n    const next = new Set<Key>(Array.from(selection))\n    inputValue.split(splitPattern).forEach((raw) => {\n      const formatted = raw\n        .trim()\n        .replace(/\\s\\s+/g, \" \")\n        .replace(/\\t|\\\\t|\\r|\\\\r|\\n|\\\\n/g, \"\")\n      if (formatted === \"\") return\n      const exists = Array.from(next).some(\n        (id) => String(id).toLocaleLowerCase() === formatted.toLocaleLowerCase()\n      )\n      if (!exists) next.add(formatted)\n    })\n    applySelection(next)\n    setInputValue(\"\")\n    setTouched(true)\n  }\n\n  function removeKeys(keys: Selection) {\n    if (selection === \"all\") return\n    const next = new Set<Key>(Array.from(selection))\n    if (keys !== \"all\") {\n      for (const k of keys) next.delete(k)\n    }\n    applySelection(next)\n    setTouched(true)\n  }\n\n  return (\n    <div className={cn(\"flex flex-col gap-y-1\", className)}>\n      <TextField\n        value={inputValue}\n        onChange={setInputValue}\n        onKeyDown={handleKeyDown}\n        onBlur={() => setTouched(true)}\n        isInvalid={isInvalid}\n        {...props}\n      >\n        {(values) => (\n          <>\n            {typeof children === \"function\" ? children(values) : children}\n            <FieldError>{isInvalid ? errorText : undefined}</FieldError>\n          </>\n        )}\n      </TextField>\n      {selection ? (\n        <TagGroup\n          disabledKeys={props.isDisabled ? new Set(list) : undefined}\n          className=\"mt-1\"\n          aria-label=\"Selected tags\"\n          {...(!props.isReadOnly && !props.isDisabled ? { onRemove: removeKeys } : {})}\n        >\n          <TagList>\n            {list.map((id) => (\n              <Tag key={id} id={id}>\n                {id}\n              </Tag>\n            ))}\n          </TagList>\n        </TagGroup>\n      ) : null}\n      <input\n        ref={hiddenRef}\n        name={name}\n        value={list.join(\",\")}\n        required={Boolean(isRequired)}\n        readOnly\n        aria-hidden=\"true\"\n        tabIndex={-1}\n        className=\"sr-only absolute -z-10 h-0 w-0 opacity-0\"\n      />\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}