{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"chat-message","type":"registry:ui","title":"ChatMessage","description":"One conversation turn, aligned and dressed by its role, streaming its body while the answer arrives, with an action rail that fades in on hover and a version navigator underneath","author":"fancy-ui <https://fancy-ui.rama.app>","categories":["ai-chat"],"registryDependencies":["https://fancy-ui.rama.app/r/internals.json","https://fancy-ui.rama.app/r/internals-dom.json","https://fancy-ui.rama.app/r/internals-markdown.json","https://fancy-ui.rama.app/r/sound.json","https://fancy-ui.rama.app/r/streaming-text.json","https://fancy-ui.rama.app/r/utils.json"],"files":[{"path":"react/src/components/chat-message/ChatMessage.tsx","content":"\"use client\";\n\nimport { forwardRef, useMemo, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"../../utils\";\nimport { StreamingText } from \"../streaming-text/StreamingText\";\nimport { formatRelativeTime } from \"../../internals/relative-time\";\nimport { useNow } from \"../../internals/use-elapsed\";\nimport { CHAT_MESSAGE_CONTEXT_KEY } from \"./types\";\nimport type { ChatMessageContext } from \"./types\";\nimport \"./chat-message.css\";\n\n/**\n * Props for ChatMessage\n */\nexport interface ChatMessageProps {\n\t/** Who produced this turn. Drives alignment, chrome, and the accessible name. */\n\trole?: \"user\" | \"assistant\" | \"system\";\n\t/** The message body. Growing strings animate — see StreamingText's contract. */\n\tcontent?: string;\n\t/** Whether `content` is still arriving. Passed through to the body renderer. */\n\tstreaming?: boolean;\n\t/** Render the body as markdown instead of a tinted plain-text stream. */\n\tmarkdown?: boolean;\n\t/** When the turn was produced. Rendered relative, with the exact time as its tooltip. */\n\ttimestamp?: Date | number;\n\t/** Rendered beside the body: an image, initials, an icon. */\n\tavatar?: ReactNode;\n\t/** Replaces the default body rendering entirely. `content` is then ignored. */\n\tchildren?: ReactNode;\n\t/** Action buttons, in a rail that fades in on hover or focus. Put `ChatMessageActions` here. */\n\tactions?: ReactNode;\n\t/** Rendered under the body — where `ChatMessageBranches` belongs. */\n\tfooter?: ReactNode;\n\t/** Additional CSS classes */\n\tclassName?: string;\n\t/**\n\t * Plays press on a message action and select on a branch step,\n\t * through the sound controller. Off by default; only audible once\n\t * the user has enabled sound. Threaded to `ChatMessageAction` and\n\t * `ChatMessageBranches` through context — enable it on one layer\n\t * only, since a sound-enabled action nested inside another\n\t * sound-enabled control would otherwise double-play.\n\t */\n\tsound?: boolean;\n}\n\nconst LABELS = {\n\tuser: \"User message\",\n\tassistant: \"Assistant message\",\n\tsystem: \"System message\",\n} as const;\n\n/**\n * One turn in a conversation: an avatar, a body that can still be arriving, a\n * relative timestamp, an action rail and a footer.\n *\n * The root element arrives through the ref channel rather than a `ref` prop,\n * per PORTING.md — the Svelte source declares `ref = $bindable(null)`.\n */\nexport const ChatMessage = forwardRef<HTMLElement, ChatMessageProps>(function ChatMessage(\n\t{\n\t\trole = \"assistant\",\n\t\tcontent = \"\",\n\t\tstreaming = false,\n\t\tmarkdown = false,\n\t\ttimestamp,\n\t\tavatar,\n\t\tchildren,\n\t\tactions,\n\t\tfooter,\n\t\tclassName,\n\t\tsound = false,\n\t},\n\tref\n) {\n\t// Pointer and focus are tracked separately and OR-ed: a pointer leaving the\n\t// message while a button inside it still holds focus must not yank the rail\n\t// out from under the keyboard.\n\tconst [pointerInside, setPointerInside] = useState(false);\n\tconst [focusInside, setFocusInside] = useState(false);\n\tconst hovered = pointerInside || focusInside;\n\n\t// Rebuilt only when something a child reads actually changes — that rebuild\n\t// is what re-renders the rail and the branch navigator below.\n\tconst context = useMemo<ChatMessageContext>(\n\t\t() => ({ role, streaming, hovered: { current: hovered }, sound }),\n\t\t[role, streaming, hovered, sound]\n\t);\n\n\tconst isUser = role === \"user\";\n\tconst isSystem = role === \"system\";\n\n\tconst time =\n\t\ttimestamp === undefined\n\t\t\t? undefined\n\t\t\t: timestamp instanceof Date\n\t\t\t\t? timestamp.getTime()\n\t\t\t\t: timestamp;\n\tconst isValidTime = time !== undefined && Number.isFinite(time);\n\n\t// A message left mounted through a long-lived session must not freeze at\n\t// whatever age it had on first paint. `useNow` is the shared clock: a thread\n\t// of fifty messages costs one interval, and it stops once the last consumer\n\t// unmounts. `formatRelativeTime` is never called with a defaulted\n\t// `Date.now()` (convention C-7) — the clock's value is always passed in.\n\tconst now = useNow();\n\n\tconst relative = isValidTime ? formatRelativeTime(time as number, { now }) : \"\";\n\tconst iso = isValidTime ? new Date(time as number).toISOString() : undefined;\n\n\tconst body = children ?? <StreamingText text={content} streaming={streaming} markdown={markdown} />;\n\n\treturn (\n\t\t<CHAT_MESSAGE_CONTEXT_KEY.Provider value={context}>\n\t\t\t<article\n\t\t\t\tref={ref}\n\t\t\t\tclassName={cn(\n\t\t\t\t\t\"ft-message flex w-full gap-3\",\n\t\t\t\t\tisUser && \"flex-row-reverse\",\n\t\t\t\t\tisSystem && \"justify-center\",\n\t\t\t\t\tclassName\n\t\t\t\t)}\n\t\t\t\tdata-role={role}\n\t\t\t\taria-label={LABELS[role]}\n\t\t\t\tonPointerEnter={() => setPointerInside(true)}\n\t\t\t\tonPointerLeave={() => setPointerInside(false)}\n\t\t\t\t// React backs `onFocus`/`onBlur` with the native focusin/focusout\n\t\t\t\t// pair, so these are the `onfocusin`/`onfocusout` of the source:\n\t\t\t\t// they fire for a button focused anywhere inside the turn.\n\t\t\t\tonFocus={() => setFocusInside(true)}\n\t\t\t\tonBlur={() => setFocusInside(false)}\n\t\t\t>\n\t\t\t\t{isSystem ? (\n\t\t\t\t\t<div className=\"text-muted-foreground flex max-w-prose flex-col items-center gap-1 py-1 text-center\">\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tclassName=\"text-xs leading-relaxed text-balance\"\n\t\t\t\t\t\t\taria-live=\"polite\"\n\t\t\t\t\t\t\taria-atomic=\"true\"\n\t\t\t\t\t\t\taria-busy={streaming}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{body}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t{relative ? (\n\t\t\t\t\t\t\t<time\n\t\t\t\t\t\t\t\tclassName=\"text-[0.6875rem] tabular-nums opacity-80\"\n\t\t\t\t\t\t\t\tdateTime={iso}\n\t\t\t\t\t\t\t\ttitle={iso}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{relative}\n\t\t\t\t\t\t\t</time>\n\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t{footer}\n\t\t\t\t\t</div>\n\t\t\t\t) : (\n\t\t\t\t\t<>\n\t\t\t\t\t\t{avatar ? <div className=\"ft-message-avatar mt-0.5 shrink-0\">{avatar}</div> : null}\n\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\"flex min-w-0 flex-col gap-1.5\",\n\t\t\t\t\t\t\t\tisUser ? \"items-end\" : \"w-full\",\n\t\t\t\t\t\t\t\tisUser && \"ft-message-capped\"\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\t\"text-sm leading-relaxed\",\n\t\t\t\t\t\t\t\t\tisUser && \"rounded-2xl px-4 py-2.5\",\n\t\t\t\t\t\t\t\t\tisUser && \"ft-message-bubble\"\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\taria-live=\"polite\"\n\t\t\t\t\t\t\t\taria-atomic=\"true\"\n\t\t\t\t\t\t\t\taria-busy={streaming}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{body}\n\t\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t\t{relative || actions ? (\n\t\t\t\t\t\t\t\t<div className={cn(\"flex items-center gap-2\", isUser && \"flex-row-reverse\")}>\n\t\t\t\t\t\t\t\t\t{relative ? (\n\t\t\t\t\t\t\t\t\t\t<time\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"text-muted-foreground text-xs tabular-nums\"\n\t\t\t\t\t\t\t\t\t\t\tdateTime={iso}\n\t\t\t\t\t\t\t\t\t\t\ttitle={iso}\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t{relative}\n\t\t\t\t\t\t\t\t\t\t</time>\n\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t\t{actions}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t) : null}\n\n\t\t\t\t\t\t\t{footer}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</>\n\t\t\t\t)}\n\t\t\t</article>\n\t\t</CHAT_MESSAGE_CONTEXT_KEY.Provider>\n\t);\n});\n","type":"registry:ui","target":"@ui/fancy/components/chat-message/ChatMessage.tsx"},{"path":"react/src/components/chat-message/ChatMessageAction.tsx","content":"\"use client\";\n\nimport { useContext, useEffect, useRef, useState } from \"react\";\nimport type { MouseEvent, ReactNode } from \"react\";\nimport { cn } from \"../../utils\";\nimport { useSoundCue } from \"../../sound/use-sound\";\nimport { CHAT_MESSAGE_CONTEXT_KEY } from \"./types\";\n\n/**\n * Props for ChatMessageAction\n */\nexport interface ChatMessageActionProps {\n\t/** Accessible name and tooltip. Required — the icon alone names nothing. */\n\tlabel: string;\n\t/** Called on click, before any confirmation label swaps in. */\n\tonClick?: (event: MouseEvent<HTMLButtonElement>) => void;\n\t/** Pressed state for a toggle. Omit entirely for a plain button. */\n\tactive?: boolean;\n\t/** Swapped in as the label for two seconds after a click, e.g. \"Copied\". */\n\tconfirmLabel?: string;\n\t/** The icon. */\n\tchildren?: ReactNode;\n\t/** Additional CSS classes */\n\tclassName?: string;\n}\n\n/** How long the confirmation label holds before the button says what it does again. */\nconst CONFIRM_MS = 2000;\n\n/** One icon button in the rail, optionally a toggle, optionally self-confirming. */\nexport function ChatMessageAction({\n\tlabel,\n\tonClick,\n\tactive,\n\tconfirmLabel,\n\tchildren,\n\tclassName,\n}: ChatMessageActionProps) {\n\t// A loose action button — used outside a `ChatMessage` root — is silent:\n\t// there is no root `sound` prop to read, so this falls through to `false`.\n\tconst message = useContext(CHAT_MESSAGE_CONTEXT_KEY);\n\tconst playCue = useSoundCue(message?.sound ?? false);\n\n\tconst [confirmed, setConfirmed] = useState(false);\n\t// A ref, not state: the timer must not wake anything that writes it.\n\tconst timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n\n\tfunction clearTimer() {\n\t\tif (timer.current !== undefined) {\n\t\t\tclearTimeout(timer.current);\n\t\t\ttimer.current = undefined;\n\t\t}\n\t}\n\n\t// Reads nothing, so it runs once and its teardown is the unmount cleanup.\n\tuseEffect(() => clearTimer, []);\n\n\tfunction handleClick(event: MouseEvent<HTMLButtonElement>) {\n\t\t// Press, never toggle-on/off: `active` is fully controlled by the caller,\n\t\t// so the post-click state is unknowable here.\n\t\tplayCue(\"press\");\n\t\tonClick?.(event);\n\t\tif (!confirmLabel) return;\n\t\tsetConfirmed(true);\n\t\t// A second click restarts the window rather than inheriting the old deadline.\n\t\tclearTimer();\n\t\ttimer.current = setTimeout(() => {\n\t\t\tsetConfirmed(false);\n\t\t\ttimer.current = undefined;\n\t\t}, CONFIRM_MS);\n\t}\n\n\tconst currentLabel = confirmed && confirmLabel ? confirmLabel : label;\n\n\treturn (\n\t\t<button\n\t\t\ttype=\"button\"\n\t\t\tclassName={cn(\n\t\t\t\t\"text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:ring-ring inline-flex size-7 shrink-0 cursor-pointer items-center justify-center rounded-md transition-colors focus-visible:ring-1 focus-visible:outline-none\",\n\t\t\t\tactive && \"bg-muted text-foreground\",\n\t\t\t\tclassName\n\t\t\t)}\n\t\t\taria-label={currentLabel}\n\t\t\ttitle={currentLabel}\n\t\t\taria-pressed={active}\n\t\t\tonClick={handleClick}\n\t\t>\n\t\t\t{children}\n\t\t</button>\n\t);\n}\n","type":"registry:ui","target":"@ui/fancy/components/chat-message/ChatMessageAction.tsx"},{"path":"react/src/components/chat-message/ChatMessageActions.tsx","content":"\"use client\";\n\nimport { useContext } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"../../utils\";\nimport { CHAT_MESSAGE_CONTEXT_KEY } from \"./types\";\nimport \"./chat-message-actions.css\";\n\n/**\n * Props for ChatMessageActions\n */\nexport interface ChatMessageActionsProps {\n\t/** Keep the rail on screen even when the message is neither hovered nor focused. */\n\talwaysVisible?: boolean;\n\t/** The buttons — `ChatMessageAction` elements. */\n\tchildren?: ReactNode;\n\t/** Additional CSS classes */\n\tclassName?: string;\n}\n\n/** The rail of buttons under a turn, revealed on hover or focus. */\nexport function ChatMessageActions({\n\talwaysVisible = false,\n\tchildren,\n\tclassName,\n}: ChatMessageActionsProps) {\n\t// Undefined when the rail is used outside a ChatMessage: it then behaves as a\n\t// plain always-hidden-until-focused button group rather than throwing.\n\tconst message = useContext(CHAT_MESSAGE_CONTEXT_KEY);\n\n\tconst visible = alwaysVisible || (message?.hovered.current ?? false);\n\n\treturn (\n\t\t<div\n\t\t\tclassName={cn(\n\t\t\t\t\"ft-message-actions flex items-center gap-0.5\",\n\t\t\t\tclassName,\n\t\t\t\tvisible && \"ft-visible\"\n\t\t\t)}\n\t\t\trole=\"group\"\n\t\t\taria-label=\"Message actions\"\n\t\t>\n\t\t\t{children}\n\t\t</div>\n\t);\n}\n","type":"registry:ui","target":"@ui/fancy/components/chat-message/ChatMessageActions.tsx"},{"path":"react/src/components/chat-message/ChatMessageBranches.tsx","content":"\"use client\";\n\nimport { useContext } from \"react\";\nimport { cn } from \"../../utils\";\nimport { useSoundCue } from \"../../sound/use-sound\";\nimport { CHAT_MESSAGE_CONTEXT_KEY } from \"./types\";\n\n/**\n * Props for ChatMessageBranches\n */\nexport interface ChatMessageBranchesProps {\n\t/** Which version is on screen, 1-based. */\n\tindex: number;\n\t/** How many versions exist. */\n\tcount: number;\n\t/** Asked to show another version, by 1-based index. Required — the navigator holds no state. */\n\tonNavigate: (index: number) => void;\n\t/** Additional CSS classes */\n\tclassName?: string;\n}\n\nconst buttonClass =\n\t\"text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:ring-ring inline-flex size-6 cursor-pointer items-center justify-center rounded-md transition-colors focus-visible:ring-1 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-40\";\n\n/** The \"2/3\" version navigator that sits under a regenerated answer. */\nexport function ChatMessageBranches({\n\tindex,\n\tcount,\n\tonNavigate,\n\tclassName,\n}: ChatMessageBranchesProps) {\n\t// Used to decide which edge the navigator hugs, and to read the root's\n\t// `sound` prop; absent context is fine for both.\n\tconst message = useContext(CHAT_MESSAGE_CONTEXT_KEY);\n\tconst playCue = useSoundCue(message?.sound ?? false);\n\n\tconst atStart = index <= 1;\n\tconst atEnd = index >= count;\n\n\t// One extracted step both arrows call: the edges are already disabled\n\t// natively, but a synthetic click bypasses that, so `next` is re-checked\n\t// against the real range before anything plays or fires.\n\tfunction navigate(next: number) {\n\t\tif (next < 1 || next > count) return;\n\t\tplayCue(\"select\");\n\t\tonNavigate(next);\n\t}\n\n\treturn (\n\t\t<div\n\t\t\tclassName={cn(\n\t\t\t\t\"text-muted-foreground flex items-center gap-0.5 text-xs\",\n\t\t\t\tmessage?.role === \"user\" && \"justify-end\",\n\t\t\t\tclassName\n\t\t\t)}\n\t\t\trole=\"group\"\n\t\t\taria-label=\"Response versions\"\n\t\t>\n\t\t\t<button\n\t\t\t\ttype=\"button\"\n\t\t\t\tclassName={buttonClass}\n\t\t\t\taria-label=\"Previous version\"\n\t\t\t\tdisabled={atStart}\n\t\t\t\tonClick={() => navigate(index - 1)}\n\t\t\t>\n\t\t\t\t<svg\n\t\t\t\t\tclassName=\"size-3.5\"\n\t\t\t\t\tviewBox=\"0 0 24 24\"\n\t\t\t\t\tfill=\"none\"\n\t\t\t\t\tstroke=\"currentColor\"\n\t\t\t\t\tstrokeWidth=\"2.5\"\n\t\t\t\t\tstrokeLinecap=\"round\"\n\t\t\t\t\tstrokeLinejoin=\"round\"\n\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t>\n\t\t\t\t\t<path d=\"m15 18-6-6 6-6\" />\n\t\t\t\t</svg>\n\t\t\t</button>\n\n\t\t\t<span className=\"tabular-nums\">\n\t\t\t\t{index}/{count}\n\t\t\t</span>\n\n\t\t\t<button\n\t\t\t\ttype=\"button\"\n\t\t\t\tclassName={buttonClass}\n\t\t\t\taria-label=\"Next version\"\n\t\t\t\tdisabled={atEnd}\n\t\t\t\tonClick={() => navigate(index + 1)}\n\t\t\t>\n\t\t\t\t<svg\n\t\t\t\t\tclassName=\"size-3.5\"\n\t\t\t\t\tviewBox=\"0 0 24 24\"\n\t\t\t\t\tfill=\"none\"\n\t\t\t\t\tstroke=\"currentColor\"\n\t\t\t\t\tstrokeWidth=\"2.5\"\n\t\t\t\t\tstrokeLinecap=\"round\"\n\t\t\t\t\tstrokeLinejoin=\"round\"\n\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t>\n\t\t\t\t\t<path d=\"m9 6 6 6-6 6\" />\n\t\t\t\t</svg>\n\t\t\t</button>\n\t\t</div>\n\t);\n}\n","type":"registry:ui","target":"@ui/fancy/components/chat-message/ChatMessageBranches.tsx"},{"path":"react/src/components/chat-message/chat-message-actions.css","content":"/*\n * Hidden by opacity rather than display: the rail keeps its box, so revealing\n * it never reflows the message underneath. It stays focusable while invisible,\n * which is the point of the `:focus-within` rule — a keyboard user tabbing\n * into it makes it appear.\n */\n.ft-message-actions {\n\topacity: 0;\n}\n\n.ft-message-actions.ft-visible,\n.ft-message-actions:focus-within {\n\topacity: 1;\n}\n\n/*\n * There is no hover on a touch screen, so gating on it would hide the actions\n * for good. Those pointers get a permanently visible rail.\n */\n@media (hover: none) {\n\t.ft-message-actions {\n\t\topacity: 1;\n\t}\n}\n\n@media (prefers-reduced-motion: no-preference) {\n\t.ft-message-actions {\n\t\ttransition: opacity 150ms ease;\n\t}\n}\n","type":"registry:ui","target":"@ui/fancy/components/chat-message/chat-message-actions.css"},{"path":"react/src/components/chat-message/chat-message.css","content":"/*\n * A user turn is a bubble; an assistant turn is plain flow, the way a written\n * answer reads. Only the bubble is capped, so a long answer uses the column it\n * was given instead of being squeezed into a column-and-a-bit.\n *\n * Both rules are anchored on `.ft-message`, the component's own root class:\n * the Svelte compiler scoped them, plain CSS is global, and the descendant\n * selector restores exactly the reach the scoped rules had.\n */\n.ft-message .ft-message-capped {\n\tmax-inline-size: var(--ft-message-max-width, 85%);\n}\n\n.ft-message .ft-message-bubble {\n\tbackground: var(--ft-message-user-bg, color-mix(in oklab, currentColor 8%, transparent));\n\tcolor: var(--ft-message-user-fg, inherit);\n}\n","type":"registry:ui","target":"@ui/fancy/components/chat-message/chat-message.css"},{"path":"react/src/components/chat-message/index.ts","content":"\"use client\";\n\nexport { ChatMessage } from \"./ChatMessage\";\nexport type { ChatMessageProps } from \"./ChatMessage\";\nexport { ChatMessageActions } from \"./ChatMessageActions\";\nexport type { ChatMessageActionsProps } from \"./ChatMessageActions\";\nexport { ChatMessageAction } from \"./ChatMessageAction\";\nexport type { ChatMessageActionProps } from \"./ChatMessageAction\";\nexport { ChatMessageBranches } from \"./ChatMessageBranches\";\nexport type { ChatMessageBranchesProps } from \"./ChatMessageBranches\";\nexport { CHAT_MESSAGE_CONTEXT_KEY } from \"./types\";\nexport type { ChatMessageContext } from \"./types\";\n","type":"registry:ui","target":"@ui/fancy/components/chat-message/index.ts"},{"path":"react/src/components/chat-message/types.ts","content":"\"use client\";\n\n/**\n * The contract between the chat-message root and its parts.\n *\n * `ChatMessage` publishes a live, read-only view of the turn it is rendering;\n * every child reads it through context instead of having the same three values\n * threaded back down as props. The root rebuilds the object whenever one of\n * those values changes, which is what re-renders the parts below it — the React\n * counterpart of the Svelte context's live getters.\n */\n\nimport { createContext } from \"react\";\n\n/** What the root publishes. Children read it; only the root writes it. */\nexport interface ChatMessageContext {\n\treadonly role: \"user\" | \"assistant\" | \"system\";\n\treadonly streaming: boolean;\n\treadonly hovered: { readonly current: boolean };\n\t/**\n\t * Whether this message plays sound cues — forwarded from the root's own\n\t * `sound` prop. Optional so a part read outside a `ChatMessage` root\n\t * (`message?.sound`) falls through to `undefined`, which is the correct\n\t * \"no sound\" answer there too.\n\t */\n\treadonly sound?: boolean;\n}\n\n/**\n * The context a part reads to find the root above it. The Svelte source\n * publishes it under a `Symbol` context key; React's own context object plays\n * that role here, so the exported name is kept and the value is a\n * `React.Context` rather than a symbol:\n *\n * ```tsx\n * const message = useContext(CHAT_MESSAGE_CONTEXT_KEY);\n * ```\n *\n * Read it as optional. Every shipped part does, so a part rendered outside a\n * root degrades instead of throwing.\n */\nexport const CHAT_MESSAGE_CONTEXT_KEY = createContext<ChatMessageContext | undefined>(undefined);\nCHAT_MESSAGE_CONTEXT_KEY.displayName = \"ChatMessageContext\";\n","type":"registry:ui","target":"@ui/fancy/components/chat-message/types.ts"}],"meta":{"license":"MIT","package":"fancy-ui-react","version":"0.7.0","source":"https://fancy-ui.rama.app/docs/components/chat-message?framework=react"}}