{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tags-input",
  "type": "registry:component",
  "title": "Tags Input",
  "description": "Keyboard-friendly tag editor for adding and removing string values.",
  "dependencies": [
    "@vllnt/ui@^0.2.1"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/tags-input/tags-input.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { X } from \"lucide-react\";\n\nimport { cn } from \"@vllnt/ui\";\n\nfunction normalizeTag(tag: string) {\n  return tag.trim();\n}\n\nfunction getNormalizedTags(tags: string[]) {\n  return tags\n    .map(normalizeTag)\n    .filter(\n      (tag, index, values) => tag.length > 0 && values.indexOf(tag) === index,\n    );\n}\n\nfunction shouldAddTagFromKey(key: string) {\n  return key === \"Enter\" || key === \",\";\n}\n\ntype TagsInputStateOptions = {\n  defaultValue: string[];\n  onValueChange?: (value: string[]) => void;\n  value?: string[];\n};\n\ntype TagsInputHandlersOptions = {\n  disabled: boolean;\n  inputValue: string;\n  onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>;\n  setInputValue: React.Dispatch<React.SetStateAction<string>>;\n  tags: string[];\n  updateTags: (nextTags: string[]) => void;\n};\n\ntype TagListProps = {\n  disabled: boolean;\n  onRemove: (tag: string) => void;\n  tags: string[];\n};\n\nfunction useTagsInputState({\n  defaultValue,\n  onValueChange,\n  value,\n}: TagsInputStateOptions) {\n  const [uncontrolledValue, setUncontrolledValue] = React.useState(() =>\n    getNormalizedTags(defaultValue),\n  );\n  const isControlled = value !== undefined;\n  const tags = React.useMemo(\n    () => getNormalizedTags(value ?? uncontrolledValue),\n    [uncontrolledValue, value],\n  );\n\n  const updateTags = React.useCallback(\n    (nextTags: string[]) => {\n      const normalizedTags = getNormalizedTags(nextTags);\n\n      if (!isControlled) {\n        setUncontrolledValue(normalizedTags);\n      }\n\n      onValueChange?.(normalizedTags);\n    },\n    [isControlled, onValueChange],\n  );\n\n  return { tags, updateTags };\n}\n\nfunction useTagsInputHandlers({\n  disabled,\n  inputValue,\n  onKeyDown,\n  setInputValue,\n  tags,\n  updateTags,\n}: TagsInputHandlersOptions) {\n  const removeTag = React.useCallback(\n    (tagToRemove: string) => {\n      updateTags(tags.filter((tag) => tag !== tagToRemove));\n    },\n    [tags, updateTags],\n  );\n\n  const commitTag = React.useCallback(() => {\n    const nextTag = normalizeTag(inputValue);\n\n    if (nextTag.length === 0 || tags.includes(nextTag)) {\n      setInputValue(\"\");\n      return;\n    }\n\n    updateTags([...tags, nextTag]);\n    setInputValue(\"\");\n  }, [inputValue, setInputValue, tags, updateTags]);\n\n  const handleKeyDown = React.useCallback(\n    (event: React.KeyboardEvent<HTMLInputElement>) => {\n      onKeyDown?.(event);\n\n      if (event.defaultPrevented || disabled) {\n        return;\n      }\n\n      if (shouldAddTagFromKey(event.key)) {\n        event.preventDefault();\n        commitTag();\n        return;\n      }\n\n      if (\n        (event.key === \"Backspace\" || event.key === \"Delete\") &&\n        inputValue.length === 0\n      ) {\n        const lastTag = tags.at(-1);\n\n        if (lastTag) {\n          event.preventDefault();\n          removeTag(lastTag);\n        }\n      }\n    },\n    [commitTag, disabled, inputValue.length, onKeyDown, removeTag, tags],\n  );\n\n  return { handleKeyDown, removeTag };\n}\n\nfunction TagList({ disabled, onRemove, tags }: TagListProps) {\n  return (\n    <ul className=\"flex flex-wrap items-center gap-2\">\n      {tags.map((tag) => (\n        <li\n          className=\"flex items-center gap-1 rounded-md border bg-muted px-2 py-1 text-sm text-foreground\"\n          key={tag}\n        >\n          <span>{tag}</span>\n          <button\n            aria-label={`Remove ${tag}`}\n            className=\"rounded-sm text-muted-foreground outline-none ring-offset-background transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\"\n            disabled={disabled}\n            onClick={(event) => {\n              event.stopPropagation();\n              onRemove(tag);\n            }}\n            type=\"button\"\n          >\n            <X className=\"size-3.5\" />\n          </button>\n        </li>\n      ))}\n    </ul>\n  );\n}\n\nexport type TagsInputProps = Omit<\n  React.ComponentPropsWithoutRef<\"input\">,\n  \"defaultValue\" | \"onChange\" | \"value\"\n> & {\n  defaultValue?: string[];\n  onValueChange?: (value: string[]) => void;\n  value?: string[];\n};\n\nconst TagsInput = React.forwardRef<HTMLInputElement, TagsInputProps>(\n  (\n    {\n      className,\n      defaultValue = [],\n      disabled = false,\n      onBlur,\n      onKeyDown,\n      onValueChange,\n      placeholder = \"Add a tag\",\n      value,\n      ...props\n    },\n    ref,\n  ) => {\n    const [inputValue, setInputValue] = React.useState(\"\");\n    const { tags, updateTags } = useTagsInputState({\n      defaultValue,\n      onValueChange,\n      value,\n    });\n    const { handleKeyDown, removeTag } = useTagsInputHandlers({\n      disabled,\n      inputValue,\n      onKeyDown,\n      setInputValue,\n      tags,\n      updateTags,\n    });\n\n    return (\n      <div\n        aria-disabled={disabled || undefined}\n        className={cn(\n          \"flex min-h-10 w-full flex-wrap items-center gap-2 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background transition-colors focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2\",\n          disabled && \"cursor-not-allowed opacity-50\",\n          className,\n        )}\n        data-disabled={disabled ? \"true\" : undefined}\n        role=\"group\"\n      >\n        <TagList disabled={disabled} onRemove={removeTag} tags={tags} />\n        <input\n          {...props}\n          className=\"min-w-[8rem] flex-1 border-0 bg-transparent p-0 text-sm placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed\"\n          disabled={disabled}\n          onBlur={onBlur}\n          onChange={(event) => {\n            setInputValue(event.target.value);\n          }}\n          onKeyDown={handleKeyDown}\n          placeholder={placeholder}\n          ref={ref}\n          type=\"text\"\n          value={inputValue}\n        />\n      </div>\n    );\n  },\n);\nTagsInput.displayName = \"TagsInput\";\n\nexport { TagsInput };\n",
      "type": "registry:component"
    }
  ],
  "version": "0.2.1",
  "stability": "stable"
}
