{"name":"tag-input","title":"Tag Input","description":"Create tags from typed input and enter press.","type":"registry:ui","docs":"/components/tag-input","categories":["forms","data-entry"],"registryDependencies":["https://pb-ui-five.vercel.app/registry/badge","https://pb-ui-five.vercel.app/registry/button","https://pb-ui-five.vercel.app/registry/input"],"files":[{"path":"components/ui/tag-input.tsx","target":"components/ui/tag-input.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { X } from \"lucide-react\";\nimport * as React from \"react\";\n\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { cn } from \"@/lib/utils\";\n\nexport type TagInputProps = Omit<React.ComponentProps<\"div\">, \"onChange\"> & {\n  value: string[];\n  onValueChange: (value: string[]) => void;\n  placeholder?: string;\n  maxTags?: number;\n  allowDuplicates?: boolean;\n  disabled?: boolean;\n  showClear?: boolean;\n};\n\nexport function TagInput({\n  value,\n  onValueChange,\n  placeholder = \"Add a tag…\",\n  maxTags,\n  allowDuplicates = false,\n  disabled = false,\n  showClear = false,\n  className,\n  ...props\n}: TagInputProps) {\n  const [inputValue, setInputValue] = React.useState(\"\");\n\n  const addTag = React.useCallback(\n    (raw: string) => {\n      const tagsToAdd = raw\n        .split(/[,,]/)\n        .map((t) => t.trim())\n        .filter((t) => t.length > 0);\n\n      if (tagsToAdd.length === 0) {\n        return;\n      }\n\n      const newValue = [...value];\n      let tagsAdded = false;\n\n      for (const next of tagsToAdd) {\n        if (\n          !allowDuplicates &&\n          newValue.some((t) => t.toLowerCase() === next.toLowerCase())\n        ) {\n          continue;\n        }\n\n        if (typeof maxTags === \"number\" && newValue.length >= maxTags) {\n          break;\n        }\n\n        newValue.push(next);\n        tagsAdded = true;\n      }\n\n      if (tagsAdded) {\n        onValueChange(newValue);\n      }\n      setInputValue(\"\");\n    },\n    [allowDuplicates, maxTags, onValueChange, value],\n  );\n\n  const removeTag = React.useCallback(\n    (tag: string) => {\n      onValueChange(value.filter((item) => item !== tag));\n    },\n    [onValueChange, value],\n  );\n\n  return (\n    <div\n      data-slot=\"tag-input\"\n      className={cn(\n        \"flex flex-wrap items-center gap-2 bg-transparent dark:bg-input/30 px-2.5 py-2 border border-input aria-invalid:border-destructive aria-invalid:focus-within:border-destructive focus-within:border-ring dark:aria-invalid:border-destructive/50 rounded-md aria-invalid:focus-within:ring-destructive/50 focus-within:ring-[3px] focus-within:ring-ring/50 w-full min-h-10 text-foreground\",\n        disabled && \"opacity-50\",\n        className,\n      )}\n      {...props}\n    >\n      {value.map((tag) => (\n        <Badge\n          key={tag}\n          variant=\"secondary\"\n          className=\"group/badge relative ps-2 pe-1 border-none rounded-md h-7 font-medium text-secondary-foreground text-xs transition-colors\"\n        >\n          <span className=\"max-w-56 truncate\">{tag}</span>\n          <Button\n            type=\"button\"\n            variant=\"ghost\"\n            size=\"icon-xs\"\n            className=\"text-muted-foreground hover:text-foreground\"\n            onClick={() => removeTag(tag)}\n            disabled={disabled}\n            aria-label={`Remove ${tag}`}\n          >\n            <X className=\"size-3.5\" />\n          </Button>\n        </Badge>\n      ))}\n      <Input\n        value={inputValue}\n        onChange={(event) => setInputValue(event.target.value)}\n        onKeyDown={(event) => {\n          if (disabled) {\n            return;\n          }\n\n          if (event.key === \"Enter\" || event.key === \",\") {\n            event.preventDefault();\n            addTag(inputValue);\n            return;\n          }\n\n          if (event.key === \"Backspace\" && inputValue.length === 0) {\n            onValueChange(value.slice(0, -1));\n          }\n        }}\n        onBlur={() => {\n          if (disabled) {\n            return;\n          }\n\n          if (inputValue.trim().length > 0) {\n            addTag(inputValue);\n          }\n        }}\n        placeholder={placeholder}\n        disabled={disabled}\n        className=\"flex-1 bg-transparent shadow-none p-0 border-0 focus-visible:ring-0 w-[18ch] min-w-[18ch] h-7\"\n      />\n      {showClear && value.length > 0 && (\n        <Button\n          type=\"button\"\n          variant=\"ghost\"\n          size=\"icon-xs\"\n          className=\"ms-auto text-muted-foreground hover:text-foreground\"\n          onClick={() => onValueChange([])}\n          disabled={disabled}\n          aria-label=\"Clear all tags\"\n        >\n          <X className=\"size-3.5\" />\n        </Button>\n      )}\n    </div>\n  );\n}\n"}]}