{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "prompt-input",
  "title": "Prompt Input",
  "description": "Auto-growing prompt textarea with a submit affordance and optional toolbar slot.",
  "dependencies": [
    "@vllnt/ui@^0.2.1",
    "lucide-react"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/prompt-input/prompt-input.tsx",
      "content": "\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport { LoaderCircle, SendHorizontal } from \"lucide-react\";\n\nimport { cn } from \"@vllnt/ui\";\n\nconst SUBMIT_KEY = \"Enter\";\nconst REM_PER_ROW = 1.5;\n\nfunction autoResize(element: HTMLTextAreaElement | null): void {\n  if (element === null) {\n    return;\n  }\n  element.style.height = \"auto\";\n  element.style.height = `${element.scrollHeight}px`;\n}\n\nexport type PromptInputProps = {\n  className?: string;\n  /** Uncontrolled initial value. */\n  defaultValue?: string;\n  /** Disables editing and sending. */\n  disabled?: boolean;\n  /** Shows a spinner while a request runs. */\n  isLoading?: boolean;\n  /** Row count before the field scrolls. */\n  maxRows?: number;\n  /** Row count at rest; sets the initial height. */\n  minRows?: number;\n  /** Called with the value on submit. */\n  onSubmit?: (value: string) => void;\n  /** Called whenever the value changes. */\n  onValueChange?: (value: string) => void;\n  /** Placeholder text. */\n  placeholder?: string;\n  /** Accessible label for the submit button. */\n  submitLabel?: string;\n  /** Optional controls rendered to the left of the submit button. */\n  toolbar?: React.ReactNode;\n  /** Controlled value. */\n  value?: string;\n};\n\nfunction applyRef(\n  ref: React.Ref<HTMLTextAreaElement> | undefined,\n  node: HTMLTextAreaElement | null,\n): void {\n  if (typeof ref === \"function\") {\n    ref(node);\n    return;\n  }\n  if (ref) {\n    ref.current = node;\n  }\n}\n\nfunction useMergedRef(ref: React.Ref<HTMLTextAreaElement> | undefined): {\n  assignRef: (node: HTMLTextAreaElement | null) => void;\n  innerRef: React.RefObject<HTMLTextAreaElement | null>;\n} {\n  const innerRef = useRef<HTMLTextAreaElement>(null);\n\n  const assignRef = useCallback(\n    (node: HTMLTextAreaElement | null) => {\n      innerRef.current = node;\n      applyRef(ref, node);\n    },\n    [ref],\n  );\n\n  return { assignRef, innerRef };\n}\n\nfunction usePromptInput({\n  defaultValue,\n  isControlled,\n  onSubmit,\n  onValueChange,\n  ref,\n  value,\n}: {\n  defaultValue: string;\n  isControlled: boolean;\n  onSubmit?: (value: string) => void;\n  onValueChange?: (value: string) => void;\n  ref?: React.Ref<HTMLTextAreaElement>;\n  value?: string;\n}) {\n  const { assignRef, innerRef } = useMergedRef(ref);\n  const [internalValue, setInternalValue] = useState(defaultValue);\n  const currentValue = isControlled ? (value ?? \"\") : internalValue;\n\n  useEffect(() => {\n    autoResize(innerRef.current);\n  }, [currentValue, innerRef]);\n\n  const handleChange = useCallback(\n    (event: React.ChangeEvent<HTMLTextAreaElement>) => {\n      if (!isControlled) {\n        setInternalValue(event.target.value);\n      }\n      onValueChange?.(event.target.value);\n    },\n    [isControlled, onValueChange],\n  );\n\n  const submit = useCallback(() => {\n    if (currentValue.trim().length === 0) {\n      return;\n    }\n    onSubmit?.(currentValue);\n    if (!isControlled) {\n      setInternalValue(\"\");\n    }\n  }, [currentValue, isControlled, onSubmit]);\n\n  const handleKeyDown = useCallback(\n    (event: React.KeyboardEvent<HTMLTextAreaElement>) => {\n      if (event.key === SUBMIT_KEY && !event.shiftKey) {\n        event.preventDefault();\n        submit();\n      }\n    },\n    [submit],\n  );\n\n  const handleFormSubmit = useCallback(\n    (event: React.SyntheticEvent<HTMLFormElement>) => {\n      event.preventDefault();\n      submit();\n    },\n    [submit],\n  );\n\n  return {\n    assignRef,\n    currentValue,\n    handleChange,\n    handleFormSubmit,\n    handleKeyDown,\n  };\n}\n\nfunction PromptInputActions({\n  canSubmit,\n  isLoading,\n  submitLabel,\n  toolbar,\n}: {\n  canSubmit: boolean;\n  isLoading: boolean;\n  submitLabel: string;\n  toolbar?: React.ReactNode;\n}) {\n  return (\n    <div className=\"flex items-center justify-between gap-2\">\n      <div className=\"flex items-center gap-1\">{toolbar}</div>\n      <button\n        aria-label={submitLabel}\n        className=\"inline-flex size-8 shrink-0 items-center justify-center rounded-lg bg-primary text-primary-foreground transition-opacity hover:opacity-90 disabled:opacity-40\"\n        disabled={!canSubmit}\n        type=\"submit\"\n      >\n        {isLoading ? (\n          <LoaderCircle className=\"size-4 animate-spin\" />\n        ) : (\n          <SendHorizontal className=\"size-4\" />\n        )}\n      </button>\n    </div>\n  );\n}\n\n/**\n * Auto-growing prompt textarea with a submit affordance and optional toolbar.\n *\n * Works controlled (`value` + `onValueChange`) or uncontrolled\n * (`defaultValue`). The field grows with its content between `minRows` and\n * `maxRows`, then scrolls. Enter submits; Shift+Enter inserts a newline.\n *\n * @example\n * <PromptInput onSubmit={(text) => send(text)} />\n */\nexport const PromptInput = ({\n  className,\n  defaultValue = \"\",\n  disabled = false,\n  isLoading = false,\n  maxRows = 8,\n  minRows = 1,\n  onSubmit,\n  onValueChange,\n  placeholder = \"Send a message…\",\n  ref,\n  submitLabel = \"Send\",\n  toolbar,\n  value,\n}: PromptInputProps & { ref?: React.Ref<HTMLTextAreaElement> }) => {\n  const isControlled = value !== undefined;\n  const {\n    assignRef,\n    currentValue,\n    handleChange,\n    handleFormSubmit,\n    handleKeyDown,\n  } = usePromptInput({\n    defaultValue,\n    isControlled,\n    onSubmit,\n    onValueChange,\n    ref,\n    value,\n  });\n\n  const canSubmit = !disabled && !isLoading && currentValue.trim().length > 0;\n\n  return (\n    <form\n      className={cn(\n        \"flex flex-col gap-2 rounded-2xl border border-border bg-background p-2 shadow-sm transition-colors focus-within:ring-1 focus-within:ring-ring\",\n        className,\n      )}\n      onSubmit={handleFormSubmit}\n    >\n      <textarea\n        aria-label={placeholder}\n        className=\"w-full resize-none bg-transparent px-2 py-1.5 text-sm text-foreground outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50\"\n        disabled={disabled}\n        onChange={handleChange}\n        onKeyDown={handleKeyDown}\n        placeholder={placeholder}\n        ref={assignRef}\n        style={{\n          maxHeight: `${maxRows * REM_PER_ROW}rem`,\n          minHeight: `${minRows * REM_PER_ROW}rem`,\n          overflowY: \"auto\",\n        }}\n        value={currentValue}\n      />\n      <PromptInputActions\n        canSubmit={canSubmit}\n        isLoading={isLoading}\n        submitLabel={submitLabel}\n        toolbar={toolbar}\n      />\n    </form>\n  );\n};\nPromptInput.displayName = \"PromptInput\";\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component",
  "version": "0.2.1",
  "stability": "stable"
}
