{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "chat-composer", "type": "registry:component", "title": "Chat", "description": "Tabbed chat panel with reasoning replies and a composer.", "registryDependencies": [ "https://www.beautifului.dev/r/foundation.json" ], "files": [ { "path": "components/primitives/ChatComposer.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 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: \"Flavor 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 = [\"Flavors\", \"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 flavor 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 \n
\n {label}\n {sub}\n for {time}\n
\n

{body}

\n \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;\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(\"done\");\n const [draft, setDraft] = useState(\"\");\n const [submitted, setSubmitted] = useState(l.initialPrompt);\n const [tab, setTab] = useState(suggestions[0] ?? \"\");\n const inputRef = useRef(null);\n\n useEffect(() => {\n let t: ReturnType;\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
\n {/* header — tabs + actions */}\n
\n
\n {suggestions.map((item) => (\n 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 \n ))}\n
\n
\n {[\n ,\n ,\n ,\n ].map((icon, i) => (\n \n \n {icon}\n \n \n ))}\n
\n
\n\n {/* conversation — fixed region so the card never changes shape */}\n
\n {/* user bubble — right aligned, soft block */}\n
\n \n {submitted}\n
\n
\n\n {messages[0] && (phase === \"reply1\" || phase === \"reply2\" || phase === \"done\") ? (\n \n ) : null}\n {messages[1] && (phase === \"reply2\" || phase === \"done\") ? (\n \n ) : null}\n
\n\n {/* composer */}\n
\n 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 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
\n \n \n \n \n \n
\n
\n \n \n );\n}\n", "type": "registry:component", "target": "components/primitives/ChatComposer.tsx" } ] }