{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "prompt-input",
  "title": "PromptInput",
  "description": "Where you write to an agent. Enter sends; Send becomes Stop while it works.",
  "registryDependencies": [
    "https://creance.pages.dev/r/creance-base.json"
  ],
  "files": [
    {
      "path": "prompt-input/PromptInput.css",
      "content": ".cr-prompt {\n  display: flex;\n  flex-direction: column;\n  gap: var(--cr-space-2);\n  box-sizing: border-box;\n  padding: var(--cr-space-3);\n  border-radius: var(--cr-radius-2xl);\n  color: var(--cr-color-text-default);\n  font-family: var(--cr-font-family-sans);\n}\n\n.cr-prompt:focus-within {\n  outline: var(--cr-focus-width) solid var(--cr-color-focus);\n  outline-offset: var(--cr-focus-offset);\n}\n\n.cr-prompt__attachments {\n  display: flex;\n  flex-wrap: wrap;\n  gap: var(--cr-space-2);\n  margin: 0;\n  padding: 0;\n  list-style: none;\n}\n\n.cr-prompt__attachment {\n  display: inline-flex;\n  align-items: center;\n  gap: var(--cr-space-2);\n  padding: 6px 6px 6px 10px;\n  border-radius: var(--cr-radius-md);\n  background: var(--cr-color-bg-subtle);\n  font-size: var(--cr-font-size-sm);\n}\n\n.cr-prompt__attachment-icon {\n  display: flex;\n  color: var(--cr-color-text-muted);\n}\n\n.cr-prompt__attachment-name {\n  font-weight: var(--cr-font-weight-semibold);\n}\n\n.cr-prompt__attachment-detail {\n  color: var(--cr-color-text-muted);\n}\n\n.cr-prompt__field {\n  box-sizing: border-box;\n  width: 100%;\n  min-height: 44px;\n  max-height: 200px;\n  padding: var(--cr-space-2);\n  border: 0;\n  outline: none;\n  background: none;\n  color: inherit;\n  font: inherit;\n  font-size: var(--cr-font-size-md);\n  line-height: 1.5;\n  resize: none;\n}\n\n.cr-prompt__field::placeholder {\n  color: var(--cr-color-text-muted);\n  opacity: 1;\n}\n\n.cr-prompt__bar {\n  display: flex;\n  align-items: center;\n  gap: var(--cr-space-2);\n}\n\n.cr-prompt__hint {\n  margin-inline-start: auto;\n  color: var(--cr-color-text-muted);\n  font-size: var(--cr-font-size-xs);\n}\n\n.cr-prompt__hint kbd {\n  font-family: var(--cr-font-family-mono);\n}\n\n.cr-prompt__attach,\n.cr-prompt__remove,\n.cr-prompt__go {\n  display: grid;\n  place-items: center;\n  flex: none;\n  padding: 0;\n  border: 0;\n  border-radius: var(--cr-radius-full);\n  cursor: pointer;\n}\n\n.cr-prompt__attach {\n  width: 32px;\n  height: 32px;\n  background: var(--cr-color-bg-subtle);\n  color: var(--cr-color-text-secondary);\n  font-size: var(--cr-font-size-lg);\n}\n\n.cr-prompt__remove {\n  width: 24px;\n  height: 24px;\n  background: none;\n  color: var(--cr-color-text-muted);\n  font-size: var(--cr-font-size-xs);\n}\n\n.cr-prompt__attach:hover,\n.cr-prompt__remove:hover {\n  color: var(--cr-color-text-default);\n}\n\n.cr-prompt__go {\n  width: var(--cr-control-height-sm);\n  height: var(--cr-control-height-sm);\n  font-size: var(--cr-font-size-lg);\n}\n\n/* With no hint, the button still sits at the end. */\n.cr-prompt__bar > .cr-prompt__go:not(.cr-prompt__hint + *) {\n  margin-inline-start: auto;\n}\n\n.cr-prompt__go[data-kind='send'] {\n  background: var(--cr-color-accent-bg);\n  box-shadow: var(--cr-shadow-accent);\n  color: var(--cr-color-accent-fg);\n}\n\n.cr-prompt__go[data-kind='send']:hover {\n  background: var(--cr-color-accent-bg-hover);\n}\n\n.cr-prompt__go[data-kind='send']:disabled {\n  background: var(--cr-color-bg-subtle);\n  box-shadow: none;\n  color: var(--cr-color-text-muted);\n  cursor: not-allowed;\n}\n\n.cr-prompt__go[data-kind='stop'] {\n  background: var(--cr-color-ink-bg);\n  color: var(--cr-color-ink-fg);\n}\n\n.cr-prompt__attach:focus-visible,\n.cr-prompt__remove:focus-visible,\n.cr-prompt__go:focus-visible {\n  outline: var(--cr-focus-width) solid var(--cr-color-focus);\n  outline-offset: var(--cr-focus-offset);\n}\n",
      "type": "registry:file",
      "target": "@components/creance/prompt-input/PromptInput.css"
    },
    {
      "path": "prompt-input/PromptInput.tsx",
      "content": "'use client';\n\nimport { useEffect, useRef, useState, type ChangeEvent, type FormEvent, type KeyboardEvent, type ReactNode } from 'react';\nimport { cx } from '@/lib/creance/cx';\nimport { CrossIcon, FileIcon, ImageIcon, PlusIcon, SendIcon, StopIcon } from '@/lib/creance/icons';\nimport './PromptInput.css';\n\nexport interface PromptAttachment {\n  id: string;\n  name: string;\n  /** Shown after the name, for example \"48 KB\". */\n  detail?: string;\n  kind?: 'file' | 'image';\n}\n\nexport interface PromptInputProps {\n  /** Accessible name of the form and its text field, for example \"Message Deploy Fixer\". */\n  label: string;\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  /** Called with the trimmed text on Enter or Send. An uncontrolled field then clears. */\n  onSubmit?: (value: string) => void;\n  /** `working` turns Send into Stop. Default `ready`. */\n  status?: 'ready' | 'working';\n  onStop?: () => void;\n  placeholder?: string;\n  disabled?: boolean;\n  /** Files already attached, shown above the text. */\n  attachments?: PromptAttachment[];\n  onRemoveAttachment?: (id: string) => void;\n  /** Shows the attach button. Called with the files the person picks. */\n  onAttach?: (files: File[]) => void;\n  /** File types the picker offers, as for `<input accept>`. */\n  accept?: string;\n  /** Beside the attach button: a ModelPicker, toggles. */\n  tools?: ReactNode;\n  /** Text before the button. Default is the keyboard hint. Pass `null` for none. */\n  hint?: ReactNode;\n  sendLabel?: string;\n  stopLabel?: string;\n  attachLabel?: string;\n  className?: string;\n}\n\nconst defaultHint = (\n  <>\n    <kbd>Enter</kbd> to send · <kbd>Shift</kbd>+<kbd>Enter</kbd> for a new line\n  </>\n);\n\n/**\n * Where you write to an agent. Enter sends and Shift+Enter starts a new line. Send is the one blue button;\n * while the agent works it becomes Stop.\n */\nexport function PromptInput({\n  label,\n  value: controlled,\n  defaultValue = '',\n  onValueChange,\n  onSubmit,\n  status = 'ready',\n  onStop,\n  placeholder,\n  disabled,\n  attachments,\n  onRemoveAttachment,\n  onAttach,\n  accept,\n  tools,\n  hint = defaultHint,\n  sendLabel = 'Send',\n  stopLabel = 'Stop',\n  attachLabel = 'Attach a file',\n  className,\n}: PromptInputProps) {\n  const [own, setOwn] = useState(defaultValue);\n  const value = controlled ?? own;\n  const field = useRef<HTMLTextAreaElement>(null);\n  const picker = useRef<HTMLInputElement>(null);\n  const working = status === 'working';\n  const empty = value.trim() === '';\n\n  // Grow with the text, up to the limit in the stylesheet.\n  useEffect(() => {\n    const el = field.current;\n    if (!el) return;\n    el.style.height = 'auto';\n    el.style.height = `${el.scrollHeight}px`;\n  }, [value]);\n\n  function change(next: string) {\n    setOwn(next);\n    onValueChange?.(next);\n  }\n\n  function submit() {\n    if (empty || disabled) return;\n    onSubmit?.(value.trim());\n    change('');\n  }\n\n  function onKeyDown(e: KeyboardEvent<HTMLTextAreaElement>) {\n    // Enter while composing text in an input method confirms the composition, not the message.\n    if (e.key !== 'Enter' || e.shiftKey || e.nativeEvent.isComposing) return;\n    e.preventDefault();\n    submit();\n  }\n\n  function onPick(e: ChangeEvent<HTMLInputElement>) {\n    const files = [...(e.target.files ?? [])];\n    if (files.length) onAttach?.(files);\n    e.target.value = '';\n  }\n\n  return (\n    <form\n      aria-label={label}\n      className={cx('cr-prompt cr-glass', className)}\n      data-status={status}\n      onSubmit={(e: FormEvent) => {\n        e.preventDefault();\n        submit();\n      }}\n    >\n      {attachments && attachments.length > 0 && (\n        <ul className=\"cr-prompt__attachments\" aria-label=\"Attached files\">\n          {attachments.map((a) => (\n            <li key={a.id} className=\"cr-prompt__attachment\">\n              <span className=\"cr-prompt__attachment-icon\">{a.kind === 'image' ? <ImageIcon /> : <FileIcon />}</span>\n              <span className=\"cr-prompt__attachment-name\">{a.name}</span>\n              {a.detail && <span className=\"cr-prompt__attachment-detail\">{a.detail}</span>}\n              {onRemoveAttachment && (\n                <button type=\"button\" className=\"cr-prompt__remove\" aria-label={`Remove ${a.name}`} onClick={() => onRemoveAttachment(a.id)}>\n                  <CrossIcon />\n                </button>\n              )}\n            </li>\n          ))}\n        </ul>\n      )}\n      <textarea\n        ref={field}\n        className=\"cr-prompt__field\"\n        aria-label={label}\n        rows={1}\n        value={value}\n        placeholder={placeholder}\n        disabled={disabled}\n        onChange={(e) => change(e.target.value)}\n        onKeyDown={onKeyDown}\n      />\n      <div className=\"cr-prompt__bar\">\n        {onAttach && (\n          <>\n            <input ref={picker} type=\"file\" hidden multiple accept={accept} tabIndex={-1} onChange={onPick} />\n            <button type=\"button\" className=\"cr-prompt__attach\" aria-label={attachLabel} disabled={disabled} onClick={() => picker.current?.click()}>\n              <PlusIcon />\n            </button>\n          </>\n        )}\n        {tools}\n        {hint !== null && <span className=\"cr-prompt__hint\">{hint}</span>}\n        {working ? (\n          <button type=\"button\" className=\"cr-prompt__go\" data-kind=\"stop\" aria-label={stopLabel} onClick={onStop}>\n            <StopIcon />\n          </button>\n        ) : (\n          <button type=\"submit\" className=\"cr-prompt__go\" data-kind=\"send\" aria-label={sendLabel} disabled={empty || disabled}>\n            <SendIcon />\n          </button>\n        )}\n      </div>\n    </form>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@components/creance/prompt-input/PromptInput.tsx"
    }
  ],
  "categories": [
    "molecule",
    "chat"
  ],
  "type": "registry:ui"
}