{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tags-input",
  "title": "Tags Input",
  "description": "Keyboard-friendly input that collects values as removable tags.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "badge",
    "input"
  ],
  "files": [
    {
      "path": "components/ui/tags-input.tsx",
      "content": "'use client'\n\nimport { Badge } from '@/components/ui/badge'\nimport { Input } from '@/components/ui/input'\nimport { cn } from '@/lib/utils'\nimport { X as RemoveIcon } from 'lucide-react'\nimport React from 'react'\n\n/**\n * used for identifying the split char and use will pasting\n */\nconst SPLITTER_REGEX = /[\\n#?=&\\t,./-]+/\n\n/**\n * used for formatting the pasted element for the correct value format to be added\n */\n\nconst FORMATTING_REGEX = /^[^a-zA-Z0-9]*|[^a-zA-Z0-9]*$/g\n\ninterface TagsInputProps extends React.HTMLAttributes<HTMLDivElement> {\n  value: string[]\n  onValueChange: (value: string[]) => void\n  placeholder?: string\n  maxItems?: number\n  minItems?: number\n}\n\ninterface TagsInputContextProps {\n  value: string[]\n  onValueChange: (value: any) => void\n  inputValue: string\n  setInputValue: React.Dispatch<React.SetStateAction<string>>\n  activeIndex: number\n  setActiveIndex: React.Dispatch<React.SetStateAction<number>>\n}\n\nconst TagInputContext = React.createContext<TagsInputContextProps | null>(null)\n\nexport const TagsInput = React.forwardRef<HTMLDivElement, TagsInputProps>(\n  (\n    {\n      children,\n      value,\n      onValueChange,\n      placeholder,\n      maxItems,\n      minItems,\n      className,\n      dir,\n      ...props\n    },\n    ref,\n  ) => {\n    const [activeIndex, setActiveIndex] = React.useState(-1)\n    const [inputValue, setInputValue] = React.useState('')\n    const [disableInput, setDisableInput] = React.useState(false)\n    const [disableButton, setDisableButton] = React.useState(false)\n    const [isValueSelected, setIsValueSelected] = React.useState(false)\n    const [selectedValue, setSelectedValue] = React.useState('')\n\n    const parseMinItems = minItems ?? 0\n    const parseMaxItems = maxItems ?? Infinity\n\n    const onValueChangeHandler = React.useCallback(\n      (val: string) => {\n        if (!value.includes(val) && value.length < parseMaxItems) {\n          onValueChange([...value, val])\n        }\n      },\n      [value],\n    )\n\n    const RemoveValue = React.useCallback(\n      (val: string) => {\n        if (value.includes(val) && value.length > parseMinItems) {\n          onValueChange(value.filter((item) => item !== val))\n        }\n      },\n      [value],\n    )\n\n    const handlePaste = React.useCallback(\n      (e: React.ClipboardEvent<HTMLInputElement>) => {\n        e.preventDefault()\n        const tags = e.clipboardData.getData('text').split(SPLITTER_REGEX)\n        const newValue = [...value]\n        tags.forEach((item) => {\n          const parsedItem = item.replaceAll(FORMATTING_REGEX, '').trim()\n          if (\n            parsedItem.length > 0 &&\n            !newValue.includes(parsedItem) &&\n            newValue.length < parseMaxItems\n          ) {\n            newValue.push(parsedItem)\n          }\n        })\n        onValueChange(newValue)\n        setInputValue('')\n      },\n      [value],\n    )\n\n    const handleSelect = React.useCallback(\n      (e: React.SyntheticEvent<HTMLInputElement>) => {\n        e.preventDefault()\n        const target = e.currentTarget\n        const selection = target.value.substring(\n          target.selectionStart ?? 0,\n          target.selectionEnd ?? 0,\n        )\n\n        setSelectedValue(selection)\n        setIsValueSelected(selection === inputValue)\n      },\n      [inputValue],\n    )\n\n    // ? suggest : a refactor rather then using a useEffect\n\n    React.useEffect(() => {\n      const VerifyDisable = () => {\n        if (value.length - 1 >= parseMinItems) {\n          setDisableButton(false)\n        } else {\n          setDisableButton(true)\n        }\n        if (value.length + 1 <= parseMaxItems) {\n          setDisableInput(false)\n        } else {\n          setDisableInput(true)\n        }\n      }\n      VerifyDisable()\n    }, [value])\n\n    // ? check: Under build , default option support\n    // * support : for the uncontrolled && controlled ui\n\n    /*  React.useEffect(() => {\n      if (!defaultOptions) return;\n      onValueChange([...value, ...defaultOptions]);\n    }, []); */\n\n    const handleKeyDown = React.useCallback(\n      async (e: React.KeyboardEvent<HTMLInputElement>) => {\n        e.stopPropagation()\n\n        const moveNext = () => {\n          const nextIndex =\n            activeIndex + 1 > value.length - 1 ? -1 : activeIndex + 1\n          setActiveIndex(nextIndex)\n        }\n\n        const movePrev = () => {\n          const prevIndex =\n            activeIndex - 1 < 0 ? value.length - 1 : activeIndex - 1\n          setActiveIndex(prevIndex)\n        }\n\n        const moveCurrent = () => {\n          const newIndex =\n            activeIndex - 1 <= 0\n              ? value.length - 1 === 0\n                ? -1\n                : 0\n              : activeIndex - 1\n          setActiveIndex(newIndex)\n        }\n        const target = e.currentTarget\n\n        // ? Suggest : the multi select should support the same pattern\n\n        switch (e.key) {\n          case 'ArrowLeft':\n            if (dir === 'rtl') {\n              if (value.length > 0 && activeIndex !== -1) {\n                moveNext()\n              }\n            } else {\n              if (value.length > 0 && target.selectionStart === 0) {\n                movePrev()\n              }\n            }\n            break\n\n          case 'ArrowRight':\n            if (dir === 'rtl') {\n              if (value.length > 0 && target.selectionStart === 0) {\n                movePrev()\n              }\n            } else {\n              if (value.length > 0 && activeIndex !== -1) {\n                moveNext()\n              }\n            }\n            break\n\n          case 'Backspace':\n          case 'Delete':\n            if (value.length > 0) {\n              if (activeIndex !== -1 && activeIndex < value.length) {\n                RemoveValue(value[activeIndex])\n                moveCurrent()\n              } else {\n                if (target.selectionStart === 0) {\n                  if (selectedValue === inputValue || isValueSelected) {\n                    RemoveValue(value[value.length - 1])\n                  }\n                }\n              }\n            }\n            break\n\n          case 'Escape':\n            const newIndex = activeIndex === -1 ? value.length - 1 : -1\n            setActiveIndex(newIndex)\n            break\n\n          case 'Enter':\n            if (inputValue.trim() !== '') {\n              e.preventDefault()\n              onValueChangeHandler(inputValue)\n              setInputValue('')\n            }\n            break\n        }\n      },\n      [activeIndex, value, inputValue, RemoveValue],\n    )\n\n    const mousePreventDefault = React.useCallback((e: React.MouseEvent) => {\n      e.preventDefault()\n      e.stopPropagation()\n    }, [])\n\n    const handleChange = React.useCallback(\n      (e: React.ChangeEvent<HTMLInputElement>) => {\n        setInputValue(e.currentTarget.value)\n      },\n      [],\n    )\n\n    return (\n      <TagInputContext.Provider\n        value={{\n          value,\n          onValueChange,\n          inputValue,\n          setInputValue,\n          activeIndex,\n          setActiveIndex,\n        }}\n      >\n        <div\n          {...props}\n          ref={ref}\n          dir={dir}\n          className={cn(\n            'flex items-center flex-wrap gap-1 p-1 rounded-lg bg-background overflow-hidden   ring-1 ring-muted  ',\n            {\n              'focus-within:ring-ring': activeIndex === -1,\n            },\n            className,\n          )}\n        >\n          {value.map((item, index) => (\n            <Badge\n              tabIndex={activeIndex !== -1 ? 0 : activeIndex}\n              key={item}\n              aria-disabled={disableButton}\n              data-active={activeIndex === index}\n              className={cn(\n                \"relative px-1 rounded flex items-center gap-1 data-[active='true']:ring-2 data-[active='true']:ring-muted-foreground truncate aria-disabled:opacity-50 aria-disabled:cursor-not-allowed\",\n              )}\n              variant={'secondary'}\n            >\n              <span className=\"text-xs\">{item}</span>\n              <button\n                type=\"button\"\n                aria-label={`Remove ${item} option`}\n                aria-roledescription=\"button to remove option\"\n                disabled={disableButton}\n                onMouseDown={mousePreventDefault}\n                onClick={() => RemoveValue(item)}\n                className=\"disabled:cursor-not-allowed\"\n              >\n                <span className=\"sr-only\">Remove {item} option</span>\n                <RemoveIcon className=\"h-4 w-4 hover:stroke-destructive\" />\n              </button>\n            </Badge>\n          ))}\n          <Input\n            tabIndex={0}\n            aria-label=\"input tag\"\n            disabled={disableInput}\n            onKeyDown={handleKeyDown}\n            onPaste={handlePaste}\n            value={inputValue}\n            onSelect={handleSelect}\n            onChange={activeIndex === -1 ? handleChange : undefined}\n            placeholder={placeholder}\n            onClick={() => setActiveIndex(-1)}\n            className={cn(\n              'outline-0 border-none h-7 min-w-fit flex-1 focus-visible:outline-0 focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:border-0 placeholder:text-muted-foreground px-1',\n              activeIndex !== -1 && 'caret-transparent',\n            )}\n          />\n        </div>\n      </TagInputContext.Provider>\n    )\n  },\n)\n\nTagsInput.displayName = 'TagsInput'\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "input"
  ],
  "type": "registry:ui"
}