{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tags",
  "title": "TagsField",
  "description": "Tag input field for BuzzForm v2.",
  "registryDependencies": [
    "@buzzform/init",
    "field",
    "@buzzform/copy"
  ],
  "files": [
    {
      "path": "registry/shadcn/fields/tags.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport type { TagsField as TagsFieldDef } from \"@buildnbuzz/form-react\";\nimport { useDataField } from \"@buildnbuzz/form-react\";\nimport { CopyButton } from \"../components/copy-button\";\nimport {\n  TagInput,\n  TagInputList,\n  TagInputItem,\n  TagInputControl,\n  type TagInputDelimiter,\n} from \"../components/tag-input\";\nimport {\n  Field,\n  FieldContent,\n  FieldDescription,\n  FieldError,\n  FieldGroup,\n  FieldLabel,\n} from \"@/components/ui/field\";\nimport { cn } from \"@/lib/utils\";\n\n// ---------------------------------------------------------------------------\n// UI options\n// ---------------------------------------------------------------------------\n\ninterface TagsUi {\n  /** Keys that create a new tag. Defaults to `[\"enter\"]`. */\n  delimiters?: (\"enter\" | \"comma\" | \"space\" | \"tab\")[];\n  /** Visual variant for tag chips. Defaults to `\"chips\"`. */\n  variant?: \"chips\" | \"pills\" | \"inline\";\n  /** Show a copy button that copies all tags as a comma-separated string. */\n  copyable?: boolean;\n  /** className applied to `<FieldGroup>`. */\n  className?: string;\n  /** Inline width applied to `<FieldGroup>`. */\n  width?: string | number;\n  /** Controls required asterisk visibility. */\n  asterisk?: boolean;\n  /** Text and label overrides. */\n  labels?: {\n    /** Custom text for tag count. Defaults to `\"{count} / {max} tags\"`. */\n    count?: React.ReactNode;\n  };\n}\n\n// ---------------------------------------------------------------------------\n// TagsField\n// ---------------------------------------------------------------------------\n\nexport function TagsField() {\n  const {\n    fieldApi,\n    field,\n    isDisabled,\n    isReadOnly,\n    isRequired,\n    label,\n    placeholder,\n    description,\n    errors,\n    isInvalid,\n    descriptionId,\n    errorId,\n    ariaDescribedBy,\n    handleBlur,\n  } = useDataField<TagsFieldDef>();\n\n  const ui = field.ui as TagsUi | undefined;\n\n  const delimiters = (ui?.delimiters ?? [\"enter\"]).map((d) =>\n    d.charAt(0).toUpperCase() + d.slice(1)\n  ) as TagInputDelimiter[];\n\n  const variant = ui?.variant ?? \"chips\";\n  const copyable = ui?.copyable ?? false;\n\n  const width = ui?.width;\n  const widthStyle = width\n    ? {\n        width: typeof width === \"number\" ? `${width}px` : width,\n        flex: \"0 1 auto\",\n      }\n    : undefined;\n\n  const value = Array.isArray(fieldApi.state.value)\n    ? (fieldApi.state.value as string[])\n    : [];\n\n  return (\n    <FieldGroup\n      data-field={fieldApi.name}\n      className={ui?.className}\n      style={widthStyle}\n    >\n      <Field data-invalid={isInvalid} data-disabled={isDisabled}>\n        {label && (\n          <FieldLabel htmlFor={fieldApi.name} className=\"gap-1 items-baseline\">\n            {label}\n            {isRequired && ui?.asterisk !== false ? (\n              <span className=\"text-destructive\">*</span>\n            ) : null}\n          </FieldLabel>\n        )}\n\n        <FieldContent>\n          <div className=\"relative\">\n            <TagInput\n              value={value}\n              onValueChange={fieldApi.handleChange}\n              variant={variant}\n              disabled={isDisabled}\n              readOnly={isReadOnly}\n              maxTags={field.maxTags}\n              allowDuplicates={field.allowDuplicates}\n              maxTagLength={field.maxTagLength}\n            >\n              <TagInputList className={cn(copyable && \"pr-10\")}>\n                {value.map((tag, i) => (\n                  <TagInputItem key={`${tag}-${i}`} index={i} />\n                ))}\n                <TagInputControl\n                  id={fieldApi.name}\n                  delimiters={delimiters}\n                  placeholder={placeholder || \"Add tags...\"}\n                  onBlur={handleBlur}\n                  autoComplete={field.autoComplete}\n                  aria-invalid={isInvalid}\n                  aria-describedby={ariaDescribedBy}\n                />\n              </TagInputList>\n            </TagInput>\n\n            {copyable && value.length > 0 && (\n              <CopyButton\n                value={value.join(\", \")}\n                disabled={isDisabled}\n                className=\"absolute right-0 top-0 h-full px-2.5 text-muted-foreground hover:text-foreground\"\n              />\n            )}\n          </div>\n\n          {field.maxTags !== undefined && (\n            <p className=\"mt-1 text-xs text-muted-foreground\">\n              {ui?.labels?.count ?? `${value.length} / ${field.maxTags} tags`}\n            </p>\n          )}\n        </FieldContent>\n\n        {description && !isInvalid && (\n          <FieldDescription id={descriptionId}>{description}</FieldDescription>\n        )}\n        {isInvalid && <FieldError id={errorId} errors={errors} />}\n      </Field>\n    </FieldGroup>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/buzzform/fields/tags.tsx"
    },
    {
      "path": "registry/shadcn/components/tag-input.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cva } from \"class-variance-authority\";\nimport { IconPlaceholder } from \"@/components/icon-placeholder\";\nimport { cn } from \"@/lib/utils\";\n\n// ---------------------------------------------------------------------------\n// Variants (CVA)\n// ---------------------------------------------------------------------------\n\nconst tagInputVariants = cva(\n  \"flex min-h-8 w-full flex-wrap items-center gap-1.5 rounded-lg border border-input bg-transparent bg-clip-padding px-3 py-1.5 text-sm transition-colors focus-within:border-ring focus-within:ring-3 focus-within:ring-ring/50\",\n  {\n    variants: {\n      disabled: {\n        true: \"cursor-not-allowed opacity-50\",\n      },\n      readOnly: {\n        true: \"cursor-default bg-muted/30\",\n      },\n    },\n  }\n);\n\nconst tagItemVariants = cva(\n  \"flex h-[calc(--spacing(5.25))] w-fit items-center justify-center gap-1 font-medium whitespace-nowrap text-foreground transition-all\",\n  {\n    variants: {\n      variant: {\n        chips: \"rounded-sm bg-muted px-1.5 text-xs\",\n        pills: \"rounded-full bg-muted px-3 text-xs\",\n        inline: \"rounded-none border-0 border-b-2 border-muted-foreground/30 bg-transparent px-1 text-sm\",\n      },\n    },\n    defaultVariants: {\n      variant: \"chips\",\n    },\n  }\n);\n\n// ---------------------------------------------------------------------------\n// Context\n// ---------------------------------------------------------------------------\n\ntype TagInputContextType = {\n  value: string[];\n  maxTags?: number;\n  allowDuplicates?: boolean;\n  maxTagLength?: number;\n  onValueChange?: (value: string[]) => void;\n  disabled?: boolean;\n  readOnly?: boolean;\n  variant: \"chips\" | \"pills\" | \"inline\";\n  addTag: (tag: string) => boolean;\n  removeTag: (index: number) => void;\n  inputValue: string;\n  setInputValue: (val: string) => void;\n  inputRef: React.RefObject<HTMLInputElement | null>;\n};\n\nconst TagInputContext = React.createContext<TagInputContextType | null>(null);\n\nfunction useTagInputContext() {\n  const context = React.useContext(TagInputContext);\n  if (!context) {\n    throw new Error(\"TagInput components must be used within a <TagInput />\");\n  }\n  return context;\n}\n\n// ---------------------------------------------------------------------------\n// TagInput Root\n// ---------------------------------------------------------------------------\n\nexport type TagInputDelimiter = \"Enter\" | \"Comma\" | \"Space\" | \"Tab\" | string;\n\nexport interface TagInputProps {\n  value?: string[];\n  onValueChange?: (value: string[]) => void;\n  disabled?: boolean;\n  readOnly?: boolean;\n  variant?: \"chips\" | \"pills\" | \"inline\";\n  maxTags?: number;\n  allowDuplicates?: boolean;\n  maxTagLength?: number;\n  children?: React.ReactNode;\n}\n\nexport const TagInput = ({\n  value = [],\n  onValueChange,\n  disabled,\n  readOnly,\n  variant = \"chips\",\n  maxTags,\n  allowDuplicates = false,\n  maxTagLength,\n  children,\n}: TagInputProps) => {\n  const [inputValue, setInputValue] = React.useState(\"\");\n  const inputRef = React.useRef<HTMLInputElement>(null);\n\n  const addTag = (tag: string): boolean => {\n    const trimmed = tag.trim();\n    if (!trimmed) return false;\n    if (maxTagLength && trimmed.length > maxTagLength) return false;\n    if (!allowDuplicates && value.includes(trimmed)) return false;\n    if (maxTags !== undefined && value.length >= maxTags) return false;\n\n    onValueChange?.([...value, trimmed]);\n    return true;\n  };\n\n  const removeTag = (index: number) => {\n    if (disabled || readOnly) return;\n    onValueChange?.(value.filter((_, i) => i !== index));\n  };\n\n  return (\n    <TagInputContext.Provider\n      value={{\n        value,\n        maxTags,\n        allowDuplicates,\n        maxTagLength,\n        onValueChange,\n        disabled,\n        readOnly,\n        variant,\n        addTag,\n        removeTag,\n        inputValue,\n        setInputValue,\n        inputRef,\n      }}\n    >\n      {children}\n    </TagInputContext.Provider>\n  );\n};\n\n// ---------------------------------------------------------------------------\n// TagInputList\n// ---------------------------------------------------------------------------\n\nexport type TagInputListProps = React.ComponentPropsWithoutRef<\"div\">;\n\nexport const TagInputList = React.forwardRef<HTMLDivElement, TagInputListProps>(\n  ({ className, ...props }, ref) => {\n    const { disabled, readOnly, inputRef } = useTagInputContext();\n\n    return (\n      <div\n        ref={ref}\n        onClick={() => !disabled && !readOnly && inputRef.current?.focus()}\n        className={cn(tagInputVariants({ disabled, readOnly, className }))}\n        {...props}\n      />\n    );\n  }\n);\nTagInputList.displayName = \"TagInputList\";\n\n// ---------------------------------------------------------------------------\n// TagInputItem\n// ---------------------------------------------------------------------------\n\nexport interface TagInputItemProps extends React.HTMLAttributes<HTMLDivElement> {\n  index: number;\n}\n\nexport const TagInputItem = React.forwardRef<HTMLDivElement, TagInputItemProps>(\n  ({ className, index, children, ...props }, ref) => {\n    const { variant, value, disabled, readOnly, removeTag } = useTagInputContext();\n    const tag = value[index];\n\n    return (\n      <div\n        ref={ref}\n        className={cn(tagItemVariants({ variant, className }))}\n        {...props}\n      >\n        {children ?? <span className=\"truncate max-w-40\">{tag}</span>}\n        {!disabled && !readOnly && (\n          <button\n            type=\"button\"\n            onClick={(e) => {\n              e.stopPropagation();\n              removeTag(index);\n            }}\n            className=\"ml-0.5 rounded-full p-0.5 opacity-50 hover:bg-muted-foreground/20 hover:opacity-100 focus:outline-none focus:ring-1 focus:ring-ring\"\n            aria-label={`Remove ${tag}`}\n          >\n            <IconPlaceholder\n              lucide=\"X\"\n              hugeicons=\"Cancel01Icon\"\n              tabler=\"IconX\"\n              phosphor=\"X\"\n              remixicon=\"RiCloseLine\"\n              className=\"size-3\"\n            />\n          </button>\n        )}\n      </div>\n    );\n  }\n);\nTagInputItem.displayName = \"TagInputItem\";\n\n// ---------------------------------------------------------------------------\n// TagInputControl\n// ---------------------------------------------------------------------------\n\nexport interface TagInputControlProps\n  extends Omit<React.InputHTMLAttributes<HTMLInputElement>, \"value\" | \"onChange\"> {\n  delimiters?: TagInputDelimiter[];\n}\n\nexport const TagInputControl = React.forwardRef<HTMLInputElement, TagInputControlProps>(\n  ({ className, delimiters = [\"Enter\"], placeholder, onBlur, onKeyDown, ...props }, ref) => {\n    const {\n      inputValue,\n      setInputValue,\n      addTag,\n      removeTag,\n      value: tags,\n      maxTags,\n      disabled,\n      readOnly,\n      inputRef,\n    } = useTagInputContext();\n\n    // Sync external forward ref with context ref\n    React.useImperativeHandle(ref, () => inputRef.current as HTMLInputElement);\n\n    const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {\n      if (e.key === \"Backspace\" && !inputValue && tags.length > 0) {\n        removeTag(tags.length - 1);\n        return;\n      }\n\n      const isDelimiter = delimiters.some((d) => {\n        if (d === \"Comma\" && e.key === \",\") return true;\n        if (d === \"Space\" && e.key === \" \") return true;\n        return d.toLowerCase() === e.key.toLowerCase();\n      });\n\n      if (isDelimiter && inputValue.trim()) {\n        e.preventDefault();\n        if (addTag(inputValue)) {\n          setInputValue(\"\");\n        }\n      }\n\n      onKeyDown?.(e);\n    };\n\n    const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n      const val = e.target.value;\n      const hasCommaDelimiter = delimiters.some((d) => d === \"Comma\" || d === \",\");\n\n      if (hasCommaDelimiter && val.includes(\",\")) {\n        const parts = val.split(\",\");\n        const last = parts.pop() ?? \"\";\n\n        parts.forEach((p) => {\n          addTag(p);\n        });\n\n        setInputValue(last);\n        return;\n      }\n\n      setInputValue(val);\n    };\n\n    const handleInputBlur = (e: React.FocusEvent<HTMLInputElement>) => {\n      if (inputValue.trim()) {\n        if (addTag(inputValue)) {\n          setInputValue(\"\");\n        }\n      }\n      onBlur?.(e);\n    };\n\n    if (readOnly) return null;\n\n    const canAddMore = maxTags === undefined || tags.length < maxTags;\n    if (!canAddMore) return null;\n\n    return (\n      <input\n        {...props}\n        ref={inputRef}\n        type=\"text\"\n        value={inputValue}\n        onChange={handleChange}\n        onKeyDown={handleKeyDown}\n        onBlur={handleInputBlur}\n        disabled={disabled}\n        placeholder={tags.length === 0 ? placeholder : \"\"}\n        className={cn(\n          \"flex-1 min-w-20 bg-transparent outline-none text-sm placeholder:text-muted-foreground disabled:cursor-not-allowed\",\n          className\n        )}\n        autoComplete={props.autoComplete ?? \"off\"}\n      />\n    );\n  }\n);\nTagInputControl.displayName = \"TagInputControl\";\n",
      "type": "registry:component",
      "target": "components/buzzform/components/tag-input.tsx"
    }
  ],
  "type": "registry:ui"
}