{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chat-prompt-input",
  "title": "Chat Prompt Input",
  "description": "A composer whose textarea grows with the text, with actions and a Send button that becomes Stop.",
  "registryDependencies": [
    "https://ui.ndekocode.dev/r/theme.json",
    "https://ui.ndekocode.dev/r/utils.json",
    "https://ui.ndekocode.dev/r/icons.json",
    "https://ui.ndekocode.dev/r/recipes.json",
    "https://ui.ndekocode.dev/r/button.json",
    "https://ui.ndekocode.dev/r/textarea.json",
    "https://ui.ndekocode.dev/r/tooltip.json",
    "https://ui.ndekocode.dev/r/chat-motion.json"
  ],
  "files": [
    {
      "path": "registry/ndeko/ui/chat-prompt-input.tsx",
      "content": "// Adapted from prompt-kit (MIT, Copyright (c) 2025 Julien Thibeaut).\n'use client';\n\nimport {\n  createContext,\n  useContext,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from 'react';\n\nimport { SendIcon } from '@/registry/ndeko/lib/icons';\nimport { CONTROL_RECIPE } from '@/registry/ndeko/lib/recipes';\nimport { cn } from '@/registry/ndeko/lib/utils';\nimport { Button } from '@/registry/ndeko/ui/button';\nimport { Swap } from '@/registry/ndeko/ui/chat-motion';\nimport { Textarea } from '@/registry/ndeko/ui/textarea';\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from '@/registry/ndeko/ui/tooltip';\n\ntype MaxHeight = number | string;\n\ntype PromptInputContextValue = {\n  isLoading: boolean;\n  value: string;\n  setValue: (value: string) => void;\n  maxHeight: MaxHeight;\n  onSubmit?: () => void;\n  disabled: boolean;\n  textareaRef: React.RefObject<HTMLTextAreaElement | null>;\n};\n\nconst PromptInputContext = createContext<PromptInputContextValue | null>(null);\n\nfunction usePromptInput() {\n  const context = useContext(PromptInputContext);\n  if (!context) {\n    throw new Error('usePromptInput must be used within a PromptInput');\n  }\n  return context;\n}\n\nconst INTERACTIVE_TARGET =\n  'button, a, input, select, textarea, [role=\"button\"], [role=\"option\"]';\n\nfunction isInteractiveTarget(target: EventTarget) {\n  if (!(target instanceof Element)) {\n    return false;\n  }\n  return target.closest(INTERACTIVE_TARGET) !== null;\n}\n\ntype PromptInputProps = Omit<React.ComponentProps<'div'>, 'onSubmit'> & {\n  isLoading?: boolean;\n  value?: string;\n  onValueChange?: (value: string) => void;\n  maxHeight?: MaxHeight;\n  onSubmit?: () => void;\n  disabled?: boolean;\n};\n\nfunction PromptInput({\n  isLoading = false,\n  value,\n  onValueChange,\n  maxHeight = 240,\n  onSubmit,\n  disabled = false,\n  className,\n  onClick,\n  children,\n  ...props\n}: PromptInputProps) {\n  const [internalValue, setInternalValue] = useState(value ?? '');\n  const textareaRef = useRef<HTMLTextAreaElement | null>(null);\n  const isControlled = value !== undefined;\n  const currentValue = value ?? internalValue;\n\n  function setValue(nextValue: string) {\n    if (!isControlled) {\n      setInternalValue(nextValue);\n    }\n    onValueChange?.(nextValue);\n  }\n\n  function focusTextarea(event: React.MouseEvent<HTMLDivElement>) {\n    onClick?.(event);\n    if (disabled) {\n      return;\n    }\n    if (isInteractiveTarget(event.target)) {\n      return;\n    }\n    textareaRef.current?.focus();\n  }\n\n  const context: PromptInputContextValue = {\n    isLoading,\n    value: currentValue,\n    setValue,\n    maxHeight,\n    onSubmit,\n    disabled,\n    textareaRef,\n  };\n\n  return (\n    <TooltipProvider>\n      <PromptInputContext.Provider value={context}>\n        <div\n          data-slot='prompt-input'\n          data-disabled={disabled || undefined}\n          data-loading={isLoading || undefined}\n          onClick={focusTextarea}\n          className={cn(\n            CONTROL_RECIPE,\n            'cursor-text rounded-xl p-2',\n            'focus-within:border-foreground/30 focus-within:ring-ring/25 dark:focus-within:border-foreground/25 focus-within:ring-[3px]',\n            'data-disabled:cursor-not-allowed data-disabled:opacity-60',\n            className\n          )}\n          {...props}\n        >\n          {children}\n        </div>\n      </PromptInputContext.Provider>\n    </TooltipProvider>\n  );\n}\n\nfunction maxHeightCssOf(maxHeight: MaxHeight, scrollHeight: number) {\n  if (typeof maxHeight === 'number') {\n    const height = Math.min(scrollHeight, maxHeight);\n    return `${height}px`;\n  }\n  return `min(${scrollHeight}px, ${maxHeight})`;\n}\n\ntype PromptInputTextareaProps = Omit<\n  React.ComponentProps<typeof Textarea>,\n  'ref' | 'value' | 'onChange'\n> & {\n  disableAutosize?: boolean;\n};\n\nfunction PromptInputTextarea({\n  disableAutosize = false,\n  className,\n  onKeyDown,\n  ...props\n}: PromptInputTextareaProps) {\n  const { value, setValue, maxHeight, onSubmit, disabled, textareaRef } =\n    usePromptInput();\n\n  useLayoutEffect(() => {\n    const textarea = textareaRef.current;\n    if (!textarea) {\n      return;\n    }\n    if (disableAutosize) {\n      return;\n    }\n    textarea.style.height = 'auto';\n    textarea.style.height = maxHeightCssOf(maxHeight, textarea.scrollHeight);\n  }, [value, maxHeight, disableAutosize, textareaRef]);\n\n  function handleChange(event: React.ChangeEvent<HTMLTextAreaElement>) {\n    setValue(event.target.value);\n  }\n\n  function handleKeyDown(event: React.KeyboardEvent<HTMLTextAreaElement>) {\n    onKeyDown?.(event);\n    if (event.defaultPrevented) {\n      return;\n    }\n    const isEnter = event.key === 'Enter';\n    const isNewLine = event.shiftKey;\n    const isComposing = event.nativeEvent.isComposing;\n    const shouldSubmit = isEnter && !isNewLine && !isComposing;\n    if (!shouldSubmit) {\n      return;\n    }\n    event.preventDefault();\n    onSubmit?.();\n  }\n\n  return (\n    <Textarea\n      rows={1}\n      disabled={disabled}\n      {...props}\n      ref={textareaRef}\n      value={value}\n      onChange={handleChange}\n      onKeyDown={handleKeyDown}\n      className={cn(\n        'max-h-none min-h-10 border-0 bg-transparent px-2 py-1.5 shadow-none ring-0 focus-visible:ring-0 dark:bg-transparent',\n        className\n      )}\n    />\n  );\n}\n\ntype PromptInputActionsProps = React.ComponentProps<'div'>;\n\nfunction PromptInputActions({ className, ...props }: PromptInputActionsProps) {\n  return (\n    <div\n      data-slot='prompt-input-actions'\n      className={cn('flex items-center gap-1', className)}\n      {...props}\n    />\n  );\n}\n\ntype PromptInputActionProps = Omit<\n  React.ComponentProps<typeof Tooltip>,\n  'children'\n> & {\n  tooltip: React.ReactNode;\n  /** The control the tooltip explains, usually an icon `Button` with an `aria-label`. */\n  children: React.ReactElement;\n  side?: 'top' | 'bottom' | 'left' | 'right';\n  className?: string;\n};\n\nfunction PromptInputAction({\n  tooltip,\n  children,\n  side = 'top',\n  className,\n  ...props\n}: PromptInputActionProps) {\n  const { disabled } = usePromptInput();\n\n  return (\n    <Tooltip {...props}>\n      <TooltipTrigger render={children} disabled={disabled} />\n      <TooltipContent side={side} className={className}>\n        {tooltip}\n      </TooltipContent>\n    </Tooltip>\n  );\n}\n\nfunction StopGlyph() {\n  return (\n    <span aria-hidden='true' className='size-2.5 rounded-[2px] bg-current' />\n  );\n}\n\nfunction submitLabelOf(\n  isLoading: boolean,\n  sendLabel: string,\n  stopLabel: string\n) {\n  if (isLoading) {\n    return stopLabel;\n  }\n  return sendLabel;\n}\n\nfunction submitModeOf(isLoading: boolean) {\n  if (isLoading) {\n    return 'stop';\n  }\n  return 'send';\n}\n\ntype PromptInputSubmitProps = Omit<\n  React.ComponentProps<typeof Button>,\n  'children' | 'onClick'\n> & {\n  /** Shown as Stop while `isLoading`; without it the button waits disabled. */\n  onStop?: () => void;\n  /** Overrides the text check, for a prompt that can be sent with attachments only. */\n  canSubmit?: boolean;\n  sendLabel?: string;\n  stopLabel?: string;\n};\n\nfunction PromptInputSubmit({\n  onStop,\n  canSubmit,\n  sendLabel = 'Send',\n  stopLabel = 'Stop',\n  disabled,\n  variant = 'default',\n  size = 'icon',\n  ...props\n}: PromptInputSubmitProps) {\n  const {\n    isLoading,\n    value,\n    onSubmit,\n    disabled: isInputDisabled,\n  } = usePromptInput();\n  const hasText = value.trim().length > 0;\n  const hasContent = canSubmit ?? hasText;\n  const isStoppable = isLoading && onStop !== undefined;\n  const isSendable = !isLoading && hasContent;\n  const isActionable = isStoppable || isSendable;\n  const isTurnedOff = Boolean(disabled);\n  const isDisabled = isTurnedOff || isInputDisabled || !isActionable;\n  const label = submitLabelOf(isLoading, sendLabel, stopLabel);\n  const mode = submitModeOf(isLoading);\n\n  function handleClick() {\n    if (isLoading) {\n      onStop?.();\n      return;\n    }\n    onSubmit?.();\n  }\n\n  return (\n    <Button\n      type='button'\n      variant={variant}\n      size={size}\n      aria-label={label}\n      disabled={isDisabled}\n      onClick={handleClick}\n      {...props}\n    >\n      <Swap swapKey={mode}>\n        {isLoading && <StopGlyph />}\n        {!isLoading && <SendIcon />}\n      </Swap>\n    </Button>\n  );\n}\n\nexport {\n  PromptInput,\n  PromptInputAction,\n  PromptInputActions,\n  PromptInputSubmit,\n  PromptInputTextarea,\n  usePromptInput,\n  type PromptInputProps,\n};\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "primitives",
    "chat"
  ],
  "type": "registry:ui"
}