{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chat-panel",
  "type": "registry:component",
  "title": "Chat panel",
  "description": "Tabbed chat panel with reasoning replies and a composer.",
  "categories": [
    "ai-interface"
  ],
  "registryDependencies": [
    "https://irisui.dev/r/foundation.json"
  ],
  "files": [
    {
      "path": "src/registry/components/chat-panel/chat-panel.tsx",
      "content": "\"use client\";\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 ChatPanelMessage = {\n  label: string;\n  sub: string;\n  time: string;\n  body: string;\n};\n\nconst MESSAGES: ChatPanelMessage[] = [\n  {\n    label: \"Pageviews\",\n    sub: \"capydb.dev\",\n    time: \"4s\",\n    body: \"Pulled 30 days of pageviews for /pricing and /docs.\",\n  },\n  {\n    label: \"Comparison\",\n    sub: \"Trend detection\",\n    time: \"2s\",\n    body: \"/pricing is up 12 % with stronger weekday peaks.\",\n  },\n];\n\nconst SUGGESTIONS = [\"Websites\", \"Funnels\"];\n\nexport type ChatPanelLabels = {\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: ChatPanelLabels = {\n  initialPrompt: \"Compare capydb.dev traffic to last month\",\n  placeholder: \"Ask or tag a website 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 ChatPanel({\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?: ChatPanelMessage[];\n  /** header chips (tabs) for switching context */\n  suggestions?: string[];\n  /** prominent copy strings */\n  labels?: Partial<ChatPanelLabels>;\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 undefined;\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-surface 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-button 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\">\n              <circle cx=\"12\" cy=\"12\" r=\"9\" />\n              <path d=\"M12 7v5l3 2\" />\n            </g>,\n            <g key=\"e\" fill=\"currentColor\" stroke=\"none\">\n              <circle cx=\"5\" cy=\"12\" r=\"1.8\" />\n              <circle cx=\"12\" cy=\"12\" r=\"1.8\" />\n              <circle cx=\"19\" cy=\"12\" r=\"1.8\" />\n            </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-button text-ink-3\n                transition-colors duration-100 hover:bg-hover hover:text-ink-2\"\n            >\n              <svg\n                width=\"15\"\n                height=\"15\"\n                viewBox=\"0 0 24 24\"\n                fill=\"none\"\n                stroke=\"currentColor\"\n                strokeWidth=\"2\"\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n              >\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-[12px] 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 label={messages[0].label} sub={messages[0].sub} time={messages[0].time} body={messages[0].body} />\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-button\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(--surface)\" : \"var(--ink-2)\",\n              }}\n            >\n              <svg\n                width=\"16\"\n                height=\"16\"\n                viewBox=\"0 0 24 24\"\n                fill=\"none\"\n                stroke=\"currentColor\"\n                strokeWidth=\"2.4\"\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n              >\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/iris/chat-panel/chat-panel.tsx"
    }
  ],
  "docs": "Docs and live preview: https://irisui.dev/components/chat-panel",
  "meta": {
    "kind": "component",
    "group": "AI interface",
    "export": "ChatPanel",
    "docs": "https://irisui.dev/components/chat-panel"
  }
}
