{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "extends": "none",
  "name": "tag-field",
  "title": "tag-field",
  "description": "tag-field",
  "dependencies": [
    "react-aria-components",
    "tailwind-merge"
  ],
  "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 { twMerge } from 'tailwind-merge'\nimport { FieldError } from '@/components/ui/field'\nimport { Tag, TagGroup, TagList } from '@/components/ui/tag-group'\nimport { TextField } from '@/components/ui/text-field'\n\ninterface 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={twMerge('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"
}