{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "input",
  "title": "Input",
  "author": "AlgorizeTech",
  "description": "A text input with label, helper text, error state, icon slots and password reveal. Reach for it as the default field in any form.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/components/custom/blocks/InputBlock.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport {\n  AlertCircle,\n  CornerDownLeft,\n  Eye,\n  EyeOff,\n  FileIcon,\n  FileText,\n  UploadCloud,\n  X,\n} from \"lucide-react\";\nimport React, {\n  forwardRef,\n  useCallback,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { getThemeStyles } from \"./input-theme\";\n\n// ==========================================\n// DYNAMIC THEME ENGINE\n// ==========================================\nexport type InputColor = \"indigo\" | \"rose\" | \"emerald\" | \"zinc\" | string;\nexport type InputVariant = \"default\" | \"filled\" | \"underlined\" | \"glow\";\nexport type InputSize = \"sm\" | \"md\" | \"lg\";\n\n// ==========================================\n// INPUT COMPONENT\n// ==========================================\nexport interface InputBlockProps\n  extends Omit<\n    React.InputHTMLAttributes<HTMLInputElement>,\n    \"size\" | \"color\" | \"type\"\n  > {\n  type?: \"text\" | \"password\" | \"email\" | \"number\" | \"file\";\n  variant?: InputVariant;\n  color?: InputColor;\n  inputSize?: InputSize;\n  label?: string;\n  helperText?: string;\n  error?: string;\n  leftIcon?: React.ReactNode;\n  rightIcon?: React.ReactNode;\n  onFilesChange?: (files: File[]) => void;\n  /**\n   * Autocomplete. Focusing the field lists these like a select menu, typing\n   * narrows the list, and choosing one writes the text into the field —\n   * your `onChange` fires exactly as if it had been typed.\n   */\n  suggestions?: string[];\n  /** Fired after a suggestion is written into the field. */\n  onSuggestionSelect?: (value: string) => void;\n  /** Cap on rows shown at once (default 8). */\n  maxSuggestions?: number;\n}\n\n// Walks up to the first ancestor that clips overflow (a modal, a scroll pane,\n// a card with overflow-hidden). That box, not the viewport, is what would crop\n// the suggestion list, so it decides whether the list opens up or down. Only\n// overflow-y counts — the list grows vertically, and a box clipping just\n// sideways (overflow-x: clip) leaves it free to hang below.\nconst getClippingRect = (el: HTMLElement): DOMRect | null => {\n  let node = el.parentElement;\n  while (node && node !== document.body) {\n    const { overflowY } = window.getComputedStyle(node);\n    if (/(auto|scroll|hidden|clip)/.test(overflowY)) {\n      return node.getBoundingClientRect();\n    }\n    node = node.parentElement;\n  }\n  return null;\n};\n\n// Sets the value the way React expects for both controlled and uncontrolled\n// inputs: through the native setter, then an `input` event so React's own\n// onChange runs. Assigning `input.value` directly would be swallowed by\n// React's value tracker and the consumer would never hear about it.\nconst writeInputValue = (input: HTMLInputElement, value: string) => {\n  const setter = Object.getOwnPropertyDescriptor(\n    HTMLInputElement.prototype,\n    \"value\",\n  )?.set;\n  setter?.call(input, value);\n  input.dispatchEvent(new Event(\"input\", { bubbles: true }));\n};\n\n/** The suggestion with the typed characters picked out. */\nfunction SuggestionText({\n  text,\n  query,\n  matchClassName,\n}: {\n  text: string;\n  query: string;\n  matchClassName: string;\n}) {\n  const q = query.trim();\n  const at = q ? text.toLowerCase().indexOf(q.toLowerCase()) : -1;\n  if (at < 0) return <>{text}</>;\n  return (\n    <>\n      {text.slice(0, at)}\n      <span className={cn(\"font-semibold\", matchClassName)}>\n        {text.slice(at, at + q.length)}\n      </span>\n      {text.slice(at + q.length)}\n    </>\n  );\n}\n\nexport const InputBlock = forwardRef<HTMLInputElement, InputBlockProps>(\n  (\n    {\n      type = \"text\",\n      variant = \"default\",\n      color = \"primary\",\n      inputSize = \"md\",\n      label,\n      helperText,\n      error,\n      leftIcon,\n      rightIcon,\n      disabled = false,\n      className,\n      multiple,\n      accept,\n      onFilesChange,\n      suggestions,\n      onSuggestionSelect,\n      maxSuggestions = 8,\n      ...props\n    },\n    ref,\n  ) => {\n    const [showPassword, setShowPassword] = useState(false);\n    const { theme, style } = getThemeStyles(color);\n    const hasError = Boolean(error);\n\n    // --- Autocomplete State ---\n    const hasSuggestions =\n      Boolean(suggestions?.length) && type !== \"file\" && type !== \"password\";\n    const inputRef = useRef<HTMLInputElement | null>(null);\n    const fieldRef = useRef<HTMLDivElement>(null);\n    const listRef = useRef<HTMLUListElement>(null);\n    const listId = useId();\n    const [listOpen, setListOpen] = useState(false);\n    const [listUp, setListUp] = useState(false);\n    const [activeIndex, setActiveIndex] = useState(-1);\n    // Mirror of what is in the field. For a controlled input this is\n    // `props.value`; otherwise it tracks the native input as the user types.\n    const [typed, setTyped] = useState(String(props.defaultValue ?? \"\"));\n    const query =\n      props.value !== undefined ? String(props.value) : typed;\n\n    const assignInputRef = (node: HTMLInputElement | null) => {\n      inputRef.current = node;\n      if (typeof ref === \"function\") ref(node);\n      else if (ref) ref.current = node;\n    };\n\n    const matches = hasSuggestions\n      ? (suggestions as string[])\n          .filter((item) =>\n            item.toLowerCase().includes(query.trim().toLowerCase()),\n          )\n          .slice(0, maxSuggestions)\n      : [];\n\n    const closeList = () => {\n      setListOpen(false);\n      setActiveIndex(-1);\n    };\n\n    const chooseSuggestion = (item: string) => {\n      const input = inputRef.current;\n      if (input) {\n        writeInputValue(input, item);\n        input.focus();\n      }\n      setTyped(item);\n      onSuggestionSelect?.(item);\n      closeList();\n    };\n\n    // Open upward only when the clipping box has no room below and more above.\n    // Measured before paint so the list never appears on the wrong side first.\n    useLayoutEffect(() => {\n      if (!listOpen || !fieldRef.current) return;\n      const trigger = fieldRef.current.getBoundingClientRect();\n      const clip = getClippingRect(fieldRef.current);\n      const listHeight = listRef.current?.offsetHeight ?? 240;\n      const bottomBound = Math.min(\n        window.innerHeight,\n        clip ? clip.bottom : Number.POSITIVE_INFINITY,\n      );\n      const topBound = Math.max(0, clip ? clip.top : 0);\n      const spaceBelow = bottomBound - trigger.bottom - 8;\n      const spaceAbove = trigger.top - topBound - 8;\n      setListUp(spaceBelow < listHeight && spaceAbove > spaceBelow);\n    }, [listOpen, matches.length]);\n\n    // Keep the keyboard-highlighted row in view inside the scrolling list.\n    useEffect(() => {\n      if (activeIndex < 0) return;\n      listRef.current\n        ?.querySelector<HTMLElement>(`[data-index=\"${activeIndex}\"]`)\n        ?.scrollIntoView({ block: \"nearest\" });\n    }, [activeIndex]);\n\n    // Click outside closes the list.\n    useEffect(() => {\n      if (!listOpen) return;\n      const onPointerDown = (e: MouseEvent) => {\n        if (!fieldRef.current?.contains(e.target as Node)) closeList();\n      };\n      document.addEventListener(\"mousedown\", onPointerDown);\n      return () => document.removeEventListener(\"mousedown\", onPointerDown);\n    }, [listOpen]);\n\n    const handleSuggestionKeyDown = (\n      e: React.KeyboardEvent<HTMLInputElement>,\n    ) => {\n      props.onKeyDown?.(e);\n      if (e.defaultPrevented || !hasSuggestions) return;\n\n      if (e.key === \"ArrowDown\") {\n        e.preventDefault();\n        if (!listOpen) setListOpen(true);\n        if (matches.length)\n          setActiveIndex((i) => (i + 1) % matches.length);\n      } else if (e.key === \"ArrowUp\") {\n        e.preventDefault();\n        if (!listOpen) setListOpen(true);\n        if (matches.length)\n          setActiveIndex((i) => (i <= 0 ? matches.length - 1 : i - 1));\n      } else if (e.key === \"Enter\") {\n        if (listOpen && activeIndex >= 0 && matches[activeIndex]) {\n          e.preventDefault();\n          chooseSuggestion(matches[activeIndex]);\n        }\n      } else if (e.key === \"Escape\") {\n        if (listOpen) {\n          e.preventDefault();\n          closeList();\n        }\n      } else if (e.key === \"Tab\") {\n        closeList();\n      }\n    };\n\n    // --- File Upload State ---\n    const [isDragging, setIsDragging] = useState(false);\n    const [selectedFiles, setSelectedFiles] = useState<File[]>([]);\n    const [previews, setPreviews] = useState<Record<string, string>>({});\n    const fileInputRef = useRef<HTMLInputElement>(null);\n\n    // --- Size Classes ---\n    const sizeClasses = {\n      sm: {\n        wrapper: \"min-h-9 px-3\",\n        text: \"text-xs\",\n        icon: \"w-3.5 h-3.5\",\n        dropzone: \"p-4 min-h-[100px]\",\n      },\n      md: {\n        wrapper: \"min-h-11 px-4\",\n        text: \"text-sm\",\n        icon: \"w-4 h-4\",\n        dropzone: \"p-6 min-h-[140px]\",\n      },\n      lg: {\n        wrapper: \"min-h-14 px-5\",\n        text: \"text-base\",\n        icon: \"w-5 h-5\",\n        dropzone: \"p-8 min-h-[180px]\",\n      },\n    };\n    const s = sizeClasses[inputSize];\n\n    // --- Variant Styles ---\n    const getVariantStyles = () => {\n      const baseWrapper = cn(\n        \"relative flex items-center w-full transition-all duration-300 overflow-hidden\",\n        s.wrapper,\n      );\n\n      if (disabled)\n        return {\n          wrapper: cn(\n            baseWrapper,\n            \"opacity-50 cursor-not-allowed border border-border bg-muted\",\n          ),\n          active: \"\",\n        };\n      if (hasError)\n        return {\n          wrapper: cn(\n            baseWrapper,\n            \"border border-red-500 bg-background\",\n            variant === \"underlined\" && \"border-0 border-b-2 rounded-none\",\n            variant !== \"underlined\" && \"rounded-xl\",\n          ),\n          active: \"focus-within:ring-4 focus-within:ring-red-500/10\",\n        };\n\n      switch (variant) {\n        case \"filled\":\n          return {\n            wrapper: cn(\n              baseWrapper,\n              \"bg-muted border border-transparent rounded-xl\",\n            ),\n            active: cn(\n              \"focus-within:bg-background focus-within:ring-4\",\n              theme.ring,\n            ),\n          };\n        case \"underlined\":\n          return {\n            wrapper: cn(\n              baseWrapper,\n              \"border-b-2 border-border bg-transparent rounded-none px-0\",\n            ),\n            active: theme.ring,\n          };\n        case \"glow\":\n          return {\n            wrapper: cn(\n              baseWrapper,\n              \"bg-background border border-border rounded-xl shadow-sm\",\n            ),\n            active: theme.glow,\n          };\n        case \"default\":\n        default:\n          return {\n            wrapper: cn(\n              baseWrapper,\n              \"bg-background border border-border rounded-xl shadow-sm\",\n            ),\n            active: cn(\"focus-within:ring-4\", theme.ring),\n          };\n      }\n    };\n    const styles = getVariantStyles();\n\n    // ==========================================\n    // FILE UPLOAD LOGIC\n    // ==========================================\n    const formatFileSize = (bytes: number) => {\n      if (bytes === 0) return \"0 Bytes\";\n      const k = 1024;\n      const sizes = [\"Bytes\", \"KB\", \"MB\", \"GB\"];\n      const i = Math.floor(Math.log(bytes) / Math.log(k));\n      return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + \" \" + sizes[i];\n    };\n\n    const processFiles = useCallback(\n      (newFiles: File[]) => {\n        const first = newFiles[0];\n        const validFiles = multiple\n          ? [...selectedFiles, ...newFiles]\n          : first\n            ? [first]\n            : [];\n        setSelectedFiles(validFiles);\n        if (onFilesChange) onFilesChange(validFiles);\n\n        // Generate previews for images\n        validFiles.forEach((file) => {\n          if (file.type.startsWith(\"image/\") && !previews[file.name]) {\n            const reader = new FileReader();\n            reader.onloadend = () => {\n              setPreviews((prev) => ({\n                ...prev,\n                [file.name]: reader.result as string,\n              }));\n            };\n            reader.readAsDataURL(file);\n          }\n        });\n      },\n      [multiple, selectedFiles, previews, onFilesChange],\n    );\n\n    const handleDragOver = (e: React.DragEvent) => {\n      e.preventDefault();\n      setIsDragging(true);\n    };\n    const handleDragLeave = (e: React.DragEvent) => {\n      e.preventDefault();\n      setIsDragging(false);\n    };\n    const handleDrop = (e: React.DragEvent) => {\n      e.preventDefault();\n      setIsDragging(false);\n      if (disabled) return;\n      if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {\n        processFiles(Array.from(e.dataTransfer.files));\n      }\n    };\n    const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n      if (e.target.files && e.target.files.length > 0) {\n        processFiles(Array.from(e.target.files));\n      }\n    };\n    const removeFile = (fileName: string, e: React.MouseEvent) => {\n      e.stopPropagation();\n      const updatedFiles = selectedFiles.filter((f) => f.name !== fileName);\n      setSelectedFiles(updatedFiles);\n      if (onFilesChange) onFilesChange(updatedFiles);\n      setPreviews((prev) => {\n        const newPreviews = { ...prev };\n        delete newPreviews[fileName];\n        return newPreviews;\n      });\n    };\n\n    // ==========================================\n    // RENDER: FILE TYPE\n    // ==========================================\n    if (type === \"file\") {\n      return (\n        <div\n          className={cn(\"flex flex-col gap-1.5 w-full relative\", className)}\n          style={style}\n        >\n          {label && (\n            <label\n              className={cn(\n                \"text-sm font-semibold\",\n                hasError ? \"text-red-500\" : \"text-foreground\",\n                disabled && \"opacity-50\",\n              )}\n            >\n              {label}\n            </label>\n          )}\n\n          <div\n            onClick={() => !disabled && fileInputRef.current?.click()}\n            onDragOver={handleDragOver}\n            onDragLeave={handleDragLeave}\n            onDrop={handleDrop}\n            className={cn(\n              \"w-full border-2 border-dashed rounded-xl flex flex-col items-center justify-center text-center cursor-pointer transition-all duration-200 ease-in-out group\",\n              s.dropzone,\n              disabled\n                ? \"opacity-50 cursor-not-allowed border-border bg-muted\"\n                : hasError\n                  ? \"border-red-400 bg-red-50/50 dark:bg-red-500/5\"\n                  : isDragging\n                    ? theme.dragActive\n                    : \"border-border bg-muted/50 hover:bg-muted\",\n            )}\n          >\n            <input\n              type=\"file\"\n              ref={fileInputRef}\n              className=\"hidden\"\n              multiple={multiple}\n              accept={accept}\n              onChange={handleFileChange}\n              disabled={disabled}\n              {...props}\n            />\n            <div\n              className={cn(\n                \"p-3 rounded-full mb-3 transition-colors\",\n                isDragging\n                  ? theme.bg\n                  : \"bg-card shadow-sm border border-border group-hover:scale-105\",\n              )}\n            >\n              <UploadCloud\n                className={cn(\n                  \"w-6 h-6\",\n                  !isDragging && theme.text,\n                )}\n              />\n            </div>\n            <p\n              className={cn(\"font-semibold text-foreground\", s.text)}\n            >\n              {isDragging\n                ? \"Drop files here\"\n                : \"Click or drag file to this area\"}\n            </p>\n            <p className=\"text-xs text-muted-foreground mt-1 max-w-[250px]\">\n              {accept ? `Supports ${accept}` : \"Supports all file types\"}{\" \"}\n              {multiple && \"• Upload multiple\"}\n            </p>\n          </div>\n\n          {/* File List / Previews */}\n          {selectedFiles.length > 0 && (\n            <div className=\"flex flex-col gap-2 mt-2\">\n              {selectedFiles.map((file) => (\n                <div\n                  key={file.name}\n                  className=\"flex items-center justify-between p-3 bg-card border border-border rounded-lg shadow-sm group\"\n                >\n                  <div className=\"flex items-center gap-3 overflow-hidden\">\n                    {previews[file.name] ? (\n                      <div className=\"w-10 h-10 shrink-0 rounded-md overflow-hidden bg-muted relative border border-border\">\n                        {/* A FileReader data URL — next/image cannot optimise\n                            it and would need `unoptimized`, so a plain img is\n                            both simpler and framework-neutral. */}\n                        <img\n                          src={previews[file.name]}\n                          alt=\"preview\"\n                          className=\"absolute inset-0 h-full w-full object-cover\"\n                        />\n                      </div>\n                    ) : (\n                      <div\n                        className={cn(\n                          \"w-10 h-10 shrink-0 rounded-md flex items-center justify-center\",\n                          theme.bgSoft,\n                        )}\n                      >\n                        {file.type.includes(\"pdf\") ? (\n                          <FileText className={cn(\"w-5 h-5\", theme.text)} />\n                        ) : (\n                          <FileIcon className={cn(\"w-5 h-5\", theme.text)} />\n                        )}\n                      </div>\n                    )}\n                    <div className=\"flex flex-col min-w-0\">\n                      <span className=\"text-sm font-semibold text-foreground truncate\">\n                        {file.name}\n                      </span>\n                      <span className=\"text-xs text-muted-foreground font-medium\">\n                        {formatFileSize(file.size)}\n                      </span>\n                    </div>\n                  </div>\n                  <button\n                    type=\"button\"\n                    onClick={(e) => removeFile(file.name, e)}\n                    className=\"p-2 shrink-0 rounded-md text-muted-foreground hover:bg-red-50 hover:text-red-500 dark:hover:bg-red-500/10 transition-colors\"\n                  >\n                    <X className=\"w-4 h-4\" />\n                  </button>\n                </div>\n              ))}\n            </div>\n          )}\n\n          {error && (\n            <p className=\"text-xs font-semibold text-red-500 flex items-center gap-1 mt-0.5\">\n              <AlertCircle className=\"w-3.5 h-3.5\" /> {error}\n            </p>\n          )}\n          {helperText && !error && (\n            <p className=\"text-xs font-medium text-muted-foreground mt-0.5\">\n              {helperText}\n            </p>\n          )}\n        </div>\n      );\n    }\n\n    // ==========================================\n    // RENDER: STANDARD TEXT/PASSWORD/EMAIL\n    // ==========================================\n    const isPassword = type === \"password\";\n    const inputType = isPassword ? (showPassword ? \"text\" : \"password\") : type;\n\n    return (\n      <div\n        className={cn(\"flex flex-col gap-1.5 w-full relative\", className)}\n        style={style}\n      >\n        {label && (\n          <label\n            className={cn(\n              \"text-sm font-semibold\",\n              hasError ? \"text-red-500\" : \"text-foreground\",\n              disabled && \"opacity-50\",\n            )}\n          >\n            {label}\n          </label>\n        )}\n\n        <div\n          ref={fieldRef}\n          className={cn(\"relative\", hasSuggestions && \"z-10\")}\n        >\n        <div className={cn(styles.wrapper, styles.active)}>\n          {leftIcon && (\n            <div\n              className={cn(\n                \"shrink-0 pl-4 text-muted-foreground\",\n                variant === \"underlined\" && \"pl-0 pr-3\",\n              )}\n            >\n              {leftIcon}\n            </div>\n          )}\n\n          <input\n            ref={assignInputRef}\n            type={inputType}\n            disabled={disabled}\n            className={cn(\n              \"w-full h-full bg-transparent border-none outline-none text-foreground placeholder:text-muted-foreground/70 transition-colors\",\n              s.text,\n              leftIcon && variant !== \"underlined\" ? \"pl-2.5\" : \"pl-0\",\n              variant === \"underlined\" && !leftIcon && \"pl-0\",\n              rightIcon || isPassword ? \"pr-2.5\" : \"pr-0\",\n            )}\n            {...props}\n            {...(hasSuggestions\n              ? {\n              role: \"combobox\",\n              autoComplete: \"off\",\n              \"aria-autocomplete\": \"list\",\n              \"aria-expanded\": listOpen && matches.length > 0,\n              \"aria-controls\": listId,\n              \"aria-activedescendant\":\n                listOpen && activeIndex >= 0\n                  ? `${listId}-${activeIndex}`\n                  : undefined,\n              onChange: (e: React.ChangeEvent<HTMLInputElement>) => {\n                props.onChange?.(e);\n                setTyped(e.target.value);\n                setActiveIndex(-1);\n                if (!listOpen) setListOpen(true);\n              },\n              onFocus: (e: React.FocusEvent<HTMLInputElement>) => {\n                props.onFocus?.(e);\n                setListOpen(true);\n              },\n              onKeyDown: handleSuggestionKeyDown,\n                }\n              : {})}\n          />\n\n          {isPassword && (\n            <button\n              type=\"button\"\n              onClick={() => setShowPassword(!showPassword)}\n              disabled={disabled}\n              className={cn(\n                \"shrink-0 pr-4 text-muted-foreground hover:text-muted-foreground transition-colors disabled:opacity-50\",\n                variant === \"underlined\" && \"pr-0 pl-3\",\n              )}\n            >\n              {showPassword ? (\n                <EyeOff className={s.icon} />\n              ) : (\n                <Eye className={s.icon} />\n              )}\n            </button>\n          )}\n          {rightIcon && !isPassword && (\n            <div\n              className={cn(\n                \"shrink-0 pr-4 text-muted-foreground\",\n                variant === \"underlined\" && \"pr-0 pl-3\",\n              )}\n            >\n              {rightIcon}\n            </div>\n          )}\n        </div>\n\n        {/* Suggestion list. Rows are chosen on mousedown so the field keeps\n            focus; the native `input` event then runs the consumer's onChange. */}\n        {hasSuggestions && listOpen && !disabled && (\n          <ul\n            ref={listRef}\n            id={listId}\n            role=\"listbox\"\n            aria-label={label ? `${label} suggestions` : \"Suggestions\"}\n            className={cn(\n              \"absolute left-0 right-0 z-50 max-h-[18.5rem] overflow-y-auto p-1 border border-border bg-popover text-popover-foreground rounded-xl shadow-lg\",\n              \"animate-in fade-in duration-150\",\n              listUp\n                ? cn(\n                    \"origin-bottom slide-in-from-bottom-1\",\n                    variant === \"underlined\"\n                      ? \"bottom-[100%]\"\n                      : \"bottom-[calc(100%+6px)]\",\n                  )\n                : cn(\n                    \"origin-top slide-in-from-top-1\",\n                    variant === \"underlined\"\n                      ? \"top-[100%]\"\n                      : \"top-[calc(100%+6px)]\",\n                  ),\n            )}\n          >\n            {matches.length === 0 ? (\n              <li className=\"px-3 py-2.5 text-xs text-muted-foreground\">\n                No matches for{\" \"}\n                <span className=\"font-medium text-foreground\">\n                  &ldquo;{query.trim()}&rdquo;\n                </span>\n                {\" \"}— keep typing to use it as-is.\n              </li>\n            ) : (\n              matches.map((item, index) => {\n                const isActive = index === activeIndex;\n                return (\n                  <li\n                    key={item}\n                    id={`${listId}-${index}`}\n                    data-index={index}\n                    role=\"option\"\n                    aria-selected={isActive}\n                    onMouseDown={(e) => {\n                      e.preventDefault();\n                      chooseSuggestion(item);\n                    }}\n                    onMouseEnter={() => setActiveIndex(index)}\n                    className={cn(\n                      \"flex items-center justify-between gap-3 px-3 py-2 rounded-lg cursor-pointer text-foreground\",\n                      s.text,\n                      isActive ? theme.bgSoft : \"hover:bg-muted/60\",\n                    )}\n                  >\n                    <span className=\"truncate\">\n                      <SuggestionText\n                        text={item}\n                        query={query}\n                        matchClassName={theme.text}\n                      />\n                    </span>\n                    {isActive && (\n                      <CornerDownLeft\n                        aria-hidden\n                        className={cn(\"w-3.5 h-3.5 shrink-0\", theme.text)}\n                      />\n                    )}\n                  </li>\n                );\n              })\n            )}\n          </ul>\n        )}\n        </div>\n\n        {error && (\n          <p className=\"text-xs font-semibold text-red-500 flex items-center gap-1 mt-0.5\">\n            <AlertCircle className=\"w-3.5 h-3.5\" /> {error}\n          </p>\n        )}\n        {helperText && !error && (\n          <p className=\"text-xs font-medium text-muted-foreground mt-0.5\">\n            {helperText}\n          </p>\n        )}\n      </div>\n    );\n  },\n);\nInputBlock.displayName = \"InputBlock\";\n",
      "type": "registry:component",
      "target": "components/custom/blocks/InputBlock.tsx"
    },
    {
      "path": "src/components/custom/blocks/input-theme.ts",
      "content": "\"use client\";\n\nimport React from \"react\";\n\n/** The class set one colour gives the block. */\nexport interface InputTheme {\n  ring: string;\n  text: string;\n  bg: string;\n  bgSoft: string;\n  glow: string;\n  dragActive: string;\n}\n\nconst PRIMARY: InputTheme = {\n  ring: \"focus-within:ring-primary/25 focus-within:border-primary\",\n  text: \"text-primary\",\n  bg: \"bg-primary text-primary-foreground\",\n  bgSoft: \"bg-primary/10 text-foreground\",\n  glow: \"focus-within:shadow-[0_0_20px_var(--primary)] focus-within:border-primary\",\n  dragActive: \"border-primary bg-primary/10\",\n};\n\nconst colorThemes: Record<string, InputTheme> = {\n  primary: PRIMARY,\n  lavender: {\n    ring: \"focus-within:ring-[#9b8afb]/30 focus-within:border-[#9b8afb]\",\n    text: \"text-[#9b8afb]\",\n    bg: \"bg-[#9b8afb] text-white\",\n    bgSoft:\n      \"bg-[#9b8afb]/10 text-[#9b8afb]\",\n    glow: \"focus-within:shadow-[0_0_20px_rgba(155,138,251,0.4)] focus-within:border-[#9b8afb]\",\n    dragActive: \"border-[#9b8afb] bg-[#9b8afb]/10\",\n  },\n  indigo: {\n    ring: \"focus-within:ring-indigo-500/30 focus-within:border-indigo-500\",\n    text: \"text-indigo-600 dark:text-indigo-400\",\n    bg: \"bg-indigo-600 text-white\",\n    bgSoft:\n      \"bg-indigo-50 dark:bg-indigo-500/10 text-indigo-900 dark:text-indigo-100\",\n    glow: \"focus-within:shadow-[0_0_20px_rgba(99,102,241,0.4)] focus-within:border-indigo-500\",\n    dragActive: \"border-indigo-500 bg-indigo-50 dark:bg-indigo-500/10\",\n  },\n  rose: {\n    ring: \"focus-within:ring-rose-500/30 focus-within:border-rose-500\",\n    text: \"text-rose-600 dark:text-rose-400\",\n    bg: \"bg-rose-600 text-white\",\n    bgSoft: \"bg-rose-50 dark:bg-rose-500/10 text-rose-900 dark:text-rose-100\",\n    glow: \"focus-within:shadow-[0_0_20px_rgba(244,63,94,0.4)] focus-within:border-rose-500\",\n    dragActive: \"border-rose-500 bg-rose-50 dark:bg-rose-500/10\",\n  },\n  emerald: {\n    ring: \"focus-within:ring-emerald-500/30 focus-within:border-emerald-500\",\n    text: \"text-emerald-600 dark:text-emerald-400\",\n    bg: \"bg-emerald-600 text-white\",\n    bgSoft:\n      \"bg-emerald-50 dark:bg-emerald-500/10 text-emerald-900 dark:text-emerald-100\",\n    glow: \"focus-within:shadow-[0_0_20px_rgba(16,185,129,0.4)] focus-within:border-emerald-500\",\n    dragActive: \"border-emerald-500 bg-emerald-50 dark:bg-emerald-500/10\",\n  },\n  zinc: {\n    ring: \"focus-within:ring-zinc-500/30 focus-within:border-zinc-500 dark:focus-within:ring-white/20 dark:focus-within:border-white\",\n    text: \"text-zinc-900 dark:text-zinc-100\",\n    bg: \"bg-zinc-900 dark:bg-zinc-100 text-white dark:text-zinc-900\",\n    bgSoft: \"bg-zinc-100 dark:bg-zinc-800/50 text-zinc-900 dark:text-zinc-100\",\n    glow: \"focus-within:shadow-[0_0_20px_rgba(255,255,255,0.15)] focus-within:border-zinc-900 dark:focus-within:border-white\",\n    dragActive:\n      \"border-zinc-900 dark:border-zinc-100 bg-zinc-100 dark:bg-zinc-800/50\",\n  },\n};\n\nconst hexToRgb = (hex: string) => {\n  let cleanHex = hex.replace(/^#/, \"\");\n  if (cleanHex.length === 3)\n    cleanHex = cleanHex\n      .split(\"\")\n      .map((x) => x + x)\n      .join(\"\");\n  const result = /^([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(cleanHex);\n  return result\n    ? `${parseInt(result[1] ?? \"63\", 16)}, ${parseInt(result[2] ?? \"66\", 16)}, ${parseInt(result[3] ?? \"f1\", 16)}`\n    : \"99, 102, 241\";\n};\n\nexport const getThemeStyles = (color: string): { theme: InputTheme; style: React.CSSProperties } => {\n  if (color && color.startsWith(\"#\")) {\n    const rgb = hexToRgb(color);\n    return {\n      theme: {\n        ring: \"focus-within:ring-[var(--theme-color)]/30 focus-within:border-[var(--theme-color)]\",\n        text: \"text-[var(--theme-color)]\",\n        bg: \"bg-[var(--theme-color)] text-white hover:opacity-90\",\n        bgSoft:\n          \"bg-[rgba(var(--theme-color-rgb),0.1)] text-[var(--theme-color)]\",\n        glow: \"focus-within:shadow-[0_0_20px_rgba(var(--theme-color-rgb),0.5)] focus-within:border-[var(--theme-color)]\",\n        dragActive:\n          \"border-[var(--theme-color)] bg-[rgba(var(--theme-color-rgb),0.05)]\",\n      },\n      style: {\n        \"--theme-color\": color,\n        \"--theme-color-rgb\": rgb,\n      } as React.CSSProperties,\n    };\n  }\n  return { theme: colorThemes[color] ?? PRIMARY, style: {} };\n};\n",
      "type": "registry:component",
      "target": "components/custom/blocks/input-theme.ts"
    }
  ],
  "docs": "Named exports: InputBlock, getThemeStyles (from input-theme.ts, which installs alongside). Accepts type (text | password | email | number | file), variant (default | filled | underlined | glow), inputSize, and a color token (indigo | rose | emerald | zinc, or your own). Pass `error` to switch it into the invalid state; `helperText` renders below when there is no error.",
  "categories": [
    "forms"
  ],
  "type": "registry:component"
}
