{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"chat-composer","title":"Chat","description":"Tabbed chat panel with reasoning replies and a composer.","dependencies":[],"registryDependencies":["https://ward.so/r/foundation.json"],"files":[{"path":"registry/ward/ChatComposer.tsx","content":"\"use client\";\n/*\n * MIT License\n *\n * Copyright (c) 2026 Shane Levine\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\nimport \"./styles/foundation.css\";\n\nimport { useEffect, useRef, useState } from \"react\";\n\n/* ─────────────────────────────────────────────────────────\n * CHAT — interactive panel with tabs, replies, and composer.\n * The reply sequence begins only after the user sends.\n * ───────────────────────────────────────────────────────── */\n\ntype Phase = \"idle\" | \"sent\" | \"reply1\" | \"reply2\" | \"done\";\n\n/* one scripted agent reply in the thread */\nexport type ChatMessage = {\n  label: string;\n  sub: string;\n  time: string;\n  body: string;\n};\n\nconst MESSAGES: ChatMessage[] = [\n  {\n    label: \"Sales History\",\n    sub: \"Project Data\",\n    time: \"4s\",\n    body: \"Pulled 3 summers of mint chip sales for comparison.\",\n  },\n  {\n    label: \"Comparison\",\n    sub: \"Trend Detection\",\n    time: \"2s\",\n    body: \"Mint chip is up 12% with stronger weekend peaks.\",\n  },\n];\n\nconst SUGGESTIONS = [\"Projects\", \"Suppliers\"];\n\nexport type ChatComposerLabels = {\n  /** the pre-filled prompt shown in the first user bubble */\n  initialPrompt: string;\n  /** composer input placeholder */\n  placeholder: string;\n};\n\nconst DEFAULT_LABELS: ChatComposerLabels = {\n  initialPrompt: \"Compare mint chip to last summer\",\n  placeholder: \"Prompt or tag a project with @\",\n};\n\nfunction Section({\n  label,\n  sub,\n  time,\n  body,\n  resolving,\n}: {\n  label: string;\n  sub: string;\n  time: string;\n  body: string;\n  resolving?: boolean;\n}) {\n  return (\n    <div\n      className=\"flex w-full flex-col gap-1.5 transition-[opacity,filter,transform] duration-400\"\n      style={{\n        opacity: resolving ? 0.55 : 1,\n        filter: resolving ? \"blur(0.5px)\" : \"blur(0)\",\n        transform: resolving ? \"scale(0.985)\" : \"scale(1)\",\n        transformOrigin: \"top left\",\n        transitionTimingFunction: \"cubic-bezier(0.23, 1, 0.32, 1)\",\n        animation: \"fade-up 400ms cubic-bezier(0.23,1,0.32,1) both\",\n      }}\n    >\n      <div className=\"flex items-center gap-1 text-[12px] leading-[1.3]\">\n        <span className=\"font-medium text-ink\">{label}</span>\n        <span className=\"text-ink-2\">{sub}</span>\n        <span className=\"text-ink\">for {time}</span>\n      </div>\n      <p className=\"text-[13px] leading-normal text-ink\">{body}</p>\n    </div>\n  );\n}\n\nexport default function ChatComposer({\n  messages = MESSAGES,\n  suggestions = SUGGESTIONS,\n  labels,\n  onSend,\n}: {\n  variant?: string;\n  /** scripted agent replies revealed in sequence after the user sends */\n  messages?: ChatMessage[];\n  /** header chips (tabs) for switching context */\n  suggestions?: string[];\n  /** prominent copy strings */\n  labels?: Partial<ChatComposerLabels>;\n  /** fired with the trimmed prompt text when the user sends */\n  onSend?: (text: string) => void;\n} = {}) {\n  const l = { ...DEFAULT_LABELS, ...labels };\n  const [phase, setPhase] = useState<Phase>(\"done\");\n  const [draft, setDraft] = useState(\"\");\n  const [submitted, setSubmitted] = useState(l.initialPrompt);\n  const [tab, setTab] = useState(suggestions[0] ?? \"\");\n  const inputRef = useRef<HTMLInputElement>(null);\n\n  useEffect(() => {\n    let t: ReturnType<typeof setTimeout>;\n    if (phase === \"sent\") t = setTimeout(() => setPhase(\"reply1\"), 500);\n    else if (phase === \"reply1\") t = setTimeout(() => setPhase(\"reply2\"), 1400);\n    else if (phase === \"reply2\") t = setTimeout(() => setPhase(\"done\"), 1200);\n    else return;\n    return () => clearTimeout(t);\n  }, [phase]);\n\n  const sent = phase !== \"idle\";\n  const canSend = draft.trim().length > 0;\n\n  const send = () => {\n    if (!canSend) return;\n    const text = draft.trim();\n    setSubmitted(text);\n    onSend?.(text);\n    setDraft(\"\");\n    setPhase(\"sent\");\n  };\n\n  return (\n    <div className=\"flex h-[288px] w-full max-w-95 flex-col self-start overflow-hidden rounded-[14px] bg-card shadow-card\">\n      {/* header — tabs + actions */}\n      <div className=\"flex shrink-0 items-center justify-between border-b border-line p-1.5\">\n        <div className=\"flex items-center\">\n          {suggestions.map((item) => (\n            <button\n              key={item}\n              type=\"button\"\n              aria-pressed={tab === item}\n              onClick={() => setTab(item)}\n              className={`rounded-[6px] px-2 py-[3px] text-[13px] text-ink transition-[background-color,opacity] duration-100 ${tab === item ? \"bg-field\" : \"opacity-50 hover:opacity-75\"}`}\n            >\n              {item}\n            </button>\n          ))}\n        </div>\n        <div className=\"flex items-center gap-1\">\n          {[\n            <path key=\"p\" d=\"M12 5v14M5 12h14\" />,\n            <g key=\"h\"><circle cx=\"12\" cy=\"12\" r=\"9\" /><path d=\"M12 7v5l3 2\" /></g>,\n            <g key=\"e\" fill=\"currentColor\" stroke=\"none\"><circle cx=\"5\" cy=\"12\" r=\"1.8\" /><circle cx=\"12\" cy=\"12\" r=\"1.8\" /><circle cx=\"19\" cy=\"12\" r=\"1.8\" /></g>,\n          ].map((icon, i) => (\n            <button\n              key={i}\n              type=\"button\"\n              aria-label=\"Action\"\n              className=\"flex size-6 items-center justify-center rounded-[6px] text-ink-3\n                transition-colors duration-100 hover:bg-hover hover:text-ink-2\"\n            >\n              <svg width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n                {icon}\n              </svg>\n            </button>\n          ))}\n        </div>\n      </div>\n\n      {/* conversation — fixed region so the card never changes shape */}\n      <div className=\"flex min-h-0 flex-1 flex-col gap-2.5 overflow-y-auto px-3 pt-2.5 pb-1\">\n        {/* user bubble — right aligned, soft block */}\n        <div className=\"flex justify-end pl-14\">\n          <div\n            className=\"rounded-xl bg-field px-3 py-1.5 text-[13px] leading-[1.4] text-ink\n              transition-[opacity,transform] duration-300\"\n            style={{\n              opacity: sent ? 1 : 0,\n              transform: sent ? \"translateY(0)\" : \"translateY(10px)\",\n              transitionTimingFunction: \"cubic-bezier(0.23, 1, 0.32, 1)\",\n            }}\n          >\n            {submitted}\n          </div>\n        </div>\n\n        {messages[0] && (phase === \"reply1\" || phase === \"reply2\" || phase === \"done\") ? (\n          <Section\n            label={messages[0].label}\n            sub={messages[0].sub}\n            time={messages[0].time}\n            body={messages[0].body}\n          />\n        ) : null}\n        {messages[1] && (phase === \"reply2\" || phase === \"done\") ? (\n          <Section\n            label={messages[1].label}\n            sub={messages[1].sub}\n            time={messages[1].time}\n            body={messages[1].body}\n            resolving={phase === \"reply2\"}\n          />\n        ) : null}\n      </div>\n\n      {/* composer */}\n      <div className=\"mt-auto shrink-0 p-1.5\">\n        <div\n          role=\"presentation\"\n          onClick={() => inputRef.current?.focus()}\n          className=\"flex cursor-text flex-col gap-2 rounded-control border border-line bg-field p-2.5 shadow-[0_1px_2px_rgba(0,0,0,0.035)] transition-[border-color,box-shadow] duration-150 focus-within:border-line-strong focus-within:shadow-[0_1px_2px_rgba(0,0,0,0.025)]\"\n        >\n          <input\n            ref={inputRef}\n            value={draft}\n            onChange={(event) => setDraft(event.target.value)}\n            onKeyDown={(event) => {\n              if (event.key === \"Enter\") send();\n            }}\n            placeholder={l.placeholder}\n            aria-label=\"Chat prompt\"\n            className=\"min-h-4.5 bg-transparent text-[13px] leading-[1.4] text-ink outline-none placeholder:text-ink-3\"\n          />\n          <div className=\"flex items-center justify-end\">\n            <button\n              type=\"button\"\n              aria-label=\"Send\"\n              disabled={!canSend}\n              onClick={send}\n              className=\"flex size-7 items-center justify-center rounded-[8px]\n                transition-[background-color,color,transform] duration-200 enabled:active:scale-[0.96]\"\n              style={{\n                background: canSend ? \"var(--ink)\" : \"var(--line-strong)\",\n                color: canSend ? \"var(--card)\" : \"var(--ink-2)\",\n              }}\n            >\n              <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.4\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n                <path d=\"M12 19V5M5 12l7-7 7 7\" />\n              </svg>\n            </button>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n","type":"registry:component","target":"components/ward/ChatComposer.tsx"}],"meta":{"variants":["Default"],"version":"1.1.0","source":"https://github.com/slev12397/beautiful-ui/blob/44a274e598395ab61e7c96c26fda2758780253b7/components/primitives/ChatComposer.tsx","access":"free"},"categories":["ai"],"type":"registry:component"}