{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "prompt-composer",
  "type": "registry:ui",
  "title": "Agent prompt composer",
  "description": "Combines sources, attachments, model choice, voice, and sending into one focused input surface.",
  "dependencies": [
    "motion"
  ],
  "categories": [
    "agent"
  ],
  "docs": "https://motion-lexicon.pages.dev/en/components/prompt-composer/",
  "meta": {
    "engines": [
      "motion"
    ],
    "runtimeCost": "light",
    "signature": "The source menu reveals from the composer anchor"
  },
  "files": [
    {
      "path": "src/registry/components/prompt-composer.tsx",
      "type": "registry:ui",
      "target": "components/motion-lexicon/prompt-composer.tsx",
      "content": "\"use client\";\n\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useRef, useState } from \"react\";\n\nexport type ComposerSource = {\n  id: string;\n  label: string;\n  type: \"file\" | \"web\" | \"app\";\n  connected?: boolean;\n};\nexport type PromptComposerProps = {\n  sources?: readonly ComposerSource[];\n  model?: string;\n  placeholder?: string;\n  sendLabel?: string;\n  addSourcesLabel?: string;\n  dictationLabel?: string;\n  onSubmit?: (prompt: string, sourceIds: string[]) => void;\n  className?: string;\n};\n\nexport function PromptComposer({\n  sources = [],\n  model = \"Agent 5\",\n  placeholder = \"Ask the agent to…\",\n  sendLabel = \"Send\",\n  addSourcesLabel = \"Add sources\",\n  dictationLabel = \"Start dictation\",\n  onSubmit,\n  className = \"\",\n}: PromptComposerProps) {\n  const [prompt, setPrompt] = useState(\"\");\n  const [menu, setMenu] = useState(false);\n  const [selected, setSelected] = useState<string[]>([]);\n  const [sent, setSent] = useState(false);\n  const area = useRef<HTMLTextAreaElement>(null);\n  const reduced = useReducedMotion();\n  const send = () => {\n    const value = prompt.trim();\n    if (!value) return;\n    onSubmit?.(value, selected);\n    setPrompt(\"\");\n    setSent(true);\n    window.setTimeout(() => setSent(false), 1200);\n  };\n\n  return (\n    <section\n      className={`relative w-full rounded-[10px] border border-neutral-200 bg-white p-2 text-neutral-950 dark:border-white/10 dark:bg-[#1b1b1b] dark:text-neutral-50 ${className}`}\n    >\n      {selected.length ? (\n        <div className=\"flex flex-wrap gap-1.5 px-2 pt-2\">\n          {selected.map((id) => {\n            const source = sources.find((item) => item.id === id);\n            return (\n              <button\n                key={id}\n                type=\"button\"\n                onClick={() =>\n                  setSelected((current) =>\n                    current.filter((value) => value !== id),\n                  )\n                }\n                className=\"min-h-11 rounded-lg bg-neutral-100 px-2.5 text-[10px] text-neutral-700 dark:bg-white/8 dark:text-neutral-200\"\n              >\n                @{source?.label ?? id} ×\n              </button>\n            );\n          })}\n        </div>\n      ) : null}\n      <textarea\n        ref={area}\n        value={prompt}\n        rows={3}\n        onChange={(event) => setPrompt(event.target.value)}\n        onKeyDown={(event) => {\n          if (event.key === \"Enter\" && !event.shiftKey) {\n            event.preventDefault();\n            send();\n          }\n        }}\n        placeholder={placeholder}\n        aria-label={placeholder}\n        className=\"min-h-20 w-full resize-none bg-transparent px-3 py-3 text-[14px] leading-6 outline-none placeholder:text-neutral-400\"\n      />\n      <div className=\"flex items-center gap-1 border-t border-neutral-100 px-1 pt-2 dark:border-white/8\">\n        <button\n          type=\"button\"\n          aria-expanded={menu}\n          aria-label={addSourcesLabel}\n          onClick={() => setMenu((value) => !value)}\n          className=\"grid size-11 place-items-center rounded-lg text-lg text-neutral-500 hover:bg-neutral-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-600 dark:hover:bg-white/5\"\n        >\n          ＋\n        </button>\n        <button\n          type=\"button\"\n          className=\"min-h-11 rounded-lg px-3 text-[10px] text-neutral-500 hover:bg-neutral-100 dark:hover:bg-white/5\"\n        >\n          {model}⌄\n        </button>\n        <button\n          type=\"button\"\n          aria-label={dictationLabel}\n          className=\"ml-auto grid size-11 place-items-center rounded-lg text-neutral-500 hover:bg-neutral-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-600 dark:hover:bg-white/5\"\n        >\n          ◉\n        </button>\n        <motion.button\n          type=\"button\"\n          aria-label={sendLabel}\n          disabled={!prompt.trim()}\n          onClick={send}\n          whileTap={reduced ? undefined : { scale: 0.94 }}\n          className={`grid size-11 place-items-center rounded-lg text-white outline-none focus-visible:ring-2 focus-visible:ring-blue-600 disabled:opacity-30 ${sent ? \"bg-emerald-600\" : \"bg-neutral-950 dark:bg-neutral-50 dark:text-neutral-950\"}`}\n        >\n          {sent ? \"✓\" : \"↑\"}\n        </motion.button>\n      </div>\n      <AnimatePresence>\n        {menu ? (\n          <motion.div\n            initial={reduced ? false : { opacity: 0, y: 6, scale: 0.98 }}\n            animate={{ opacity: 1, y: 0, scale: 1 }}\n            exit={{ opacity: 0, y: 4, scale: 0.98 }}\n            className=\"absolute bottom-14 left-3 z-20 w-[min(300px,calc(100%-24px))] rounded-[10px] border border-neutral-200 bg-white p-2 shadow-[0_8px_24px_rgba(0,0,0,.14)] dark:border-white/10 dark:bg-[#202020]\"\n          >\n            {sources.map((source) => (\n              <button\n                key={source.id}\n                type=\"button\"\n                onClick={() =>\n                  setSelected((current) =>\n                    current.includes(source.id)\n                      ? current.filter((id) => id !== source.id)\n                      : [...current, source.id],\n                  )\n                }\n                className=\"flex min-h-11 w-full items-center gap-3 rounded-lg px-3 text-left hover:bg-neutral-50 dark:hover:bg-white/5\"\n              >\n                <span className=\"grid size-7 place-items-center rounded-lg bg-neutral-100 text-[9px] uppercase text-neutral-500 dark:bg-white/5\">\n                  {source.type.slice(0, 1)}\n                </span>\n                <span className=\"text-[11px] font-medium\">{source.label}</span>\n                <span\n                  className={`ml-auto size-2 rounded-full ${selected.includes(source.id) ? \"bg-neutral-950 dark:bg-neutral-50\" : source.connected ? \"bg-emerald-600\" : \"bg-neutral-200\"}`}\n                />\n              </button>\n            ))}\n          </motion.div>\n        ) : null}\n      </AnimatePresence>\n    </section>\n  );\n}\n"
    }
  ]
}
