{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "retro-message",
  "title": "RetroMessage",
  "description": "Mac OS 9 chat transcript parts: a scrolling message thread, user and assistant messages built from AI SDK-style parts, and copy/retry actions.",
  "dependencies": [
    "@ui-sdk/core@0.1.0-beta.1",
    "@ui-sdk/react@0.1.0-beta.1"
  ],
  "registryDependencies": [
    "https://nostalgia-ui.com/r/nostalgia-theme.json",
    "https://nostalgia-ui.com/r/retro-ai.json",
    "https://nostalgia-ui.com/r/retro-bevel-button.json",
    "https://nostalgia-ui.com/r/retro-icons.json",
    "https://nostalgia-ui.com/r/retro-motion.json",
    "https://nostalgia-ui.com/r/retro-scrollbar.json",
    "https://nostalgia-ui.com/r/retro-streaming-text.json",
    "https://nostalgia-ui.com/r/retro-thinking.json"
  ],
  "files": [
    {
      "path": "registry/new-york/ui/retro-message.tsx",
      "content": "\"use client\"\n\n/*\n * RetroMessage: the transcript side of a Mac OS 9 AI chat.\n *\n *   <RetroMessageThread streaming={status === \"streaming\"}>   white inset well\n *     <RetroMessage message={m} streaming renderTool={…}>    parts, in order\n *       <RetroMessageActions message={m} onRetry={…} />      Copy, Try Again\n *     </RetroMessage>\n *   </RetroMessageThread>\n *\n * Crafted moments:\n *  - The thread holds the end in view while a reply types, and lets go the\n *    moment you scroll up to read. If more arrives while you're up there, a\n *    small \"New messages\" bevel button rises from the bottom edge and takes\n *    you back (UI SDK Scroll Area's stick-to-end, with a Platinum scrollbar).\n *  - Copy answers in place: the button reads \"Copied\" with a check for a\n *    moment, at the same width, so nothing beside it moves.\n *\n * Messages enter with the family's fade and 4px rise (160ms; classic and\n * reduced motion: instant). Text streams through RetroStreamingText's paced\n * reveal with the insertion caret in the last paragraph or code block.\n * Text formatting is deliberately small: blank lines make paragraphs, single\n * newlines stay, `inline code` and fenced ``` blocks render in Monaco, **bold** is bold.\n */\n\nimport * as React from \"react\"\nimport {\n  createScrollAreaController,\n  ScrollArea as ScrollAreaPrimitive,\n  ScrollAreaContent as ScrollAreaContentPrimitive,\n  ScrollAreaScrollToEnd as ScrollAreaScrollToEndPrimitive,\n  ScrollAreaViewport as ScrollAreaViewportPrimitive,\n} from \"@ui-sdk/react/scroll-area\"\n\nimport { cn } from \"@/lib/utils\"\nimport {\n  isToolPart,\n  messageText,\n  toolNameOf,\n  type RetroChatMessage,\n  type RetroChatRole,\n  type RetroFilePart,\n  type RetroMessagePart,\n  type RetroToolPart,\n} from \"@/registry/new-york/lib/retro-ai\"\nimport { useMergedRef } from \"@/registry/new-york/lib/retro-motion\"\nimport { RetroBevelButton } from \"@/registry/new-york/ui/retro-bevel-button\"\nimport { RetroIconDocument } from \"@/registry/new-york/ui/retro-icons\"\nimport { RetroScrollBar } from \"@/registry/new-york/ui/retro-scrollbar\"\nimport { RetroCaret, useRetroStreamingText } from \"@/registry/new-york/ui/retro-streaming-text\"\nimport { RetroReasoning, RetroWatchIcon } from \"@/registry/new-york/ui/retro-thinking\"\n\n/** The family's entrance: fade and rise 4px, 160ms. */\nconst ENTER =\n  \"transition-[opacity,translate] duration-160 ease-[cubic-bezier(0.23,1,0.32,1)] starting:translate-y-1 starting:opacity-0 in-data-[flair=classic]:transition-none motion-reduce:transition-none\"\n\n/* ── pixel glyphs ──────────────────────────────────────── */\n\n/** The assistant's avatar: a 16px compact Macintosh with a happy face. */\nfunction RetroAssistantIcon({ className, ...props }: React.SVGAttributes<SVGSVGElement>) {\n  return (\n    <svg\n      width=\"16\"\n      height=\"16\"\n      viewBox=\"0 0 16 16\"\n      aria-hidden=\"true\"\n      shapeRendering=\"crispEdges\"\n      className={cn(\"inline-block shrink-0\", className)}\n      {...props}\n    >\n      <rect x=\"3\" y=\"0\" width=\"11\" height=\"14\" fill=\"var(--os9-black)\" />\n      <rect x=\"4\" y=\"1\" width=\"9\" height=\"12\" fill=\"var(--os9-gray-300)\" />\n      <rect x=\"4\" y=\"1\" width=\"9\" height=\"1\" fill=\"var(--os9-white)\" />\n      <rect x=\"5\" y=\"2\" width=\"7\" height=\"6\" fill=\"var(--os9-black)\" />\n      <rect x=\"6\" y=\"3\" width=\"5\" height=\"4\" fill=\"var(--os9-white)\" />\n      <rect x=\"7\" y=\"4\" width=\"1\" height=\"1\" fill=\"var(--os9-black)\" />\n      <rect x=\"9\" y=\"4\" width=\"1\" height=\"1\" fill=\"var(--os9-black)\" />\n      <rect x=\"7\" y=\"6\" width=\"3\" height=\"1\" fill=\"var(--os9-black)\" />\n      <rect x=\"9\" y=\"10\" width=\"3\" height=\"1\" fill=\"var(--os9-black)\" />\n      <rect x=\"4\" y=\"14\" width=\"9\" height=\"2\" fill=\"var(--os9-black)\" />\n      <rect x=\"5\" y=\"14\" width=\"7\" height=\"1\" fill=\"var(--os9-gray-500)\" />\n    </svg>\n  )\n}\n\nfunction Glyph({ children, size = 12 }: { children: React.ReactNode; size?: number }) {\n  return (\n    <svg\n      width={size}\n      height={size}\n      viewBox={`0 0 ${size} ${size}`}\n      aria-hidden=\"true\"\n      shapeRendering=\"crispEdges\"\n      className=\"inline-block shrink-0\"\n    >\n      {children}\n    </svg>\n  )\n}\n\nconst CopyGlyph = () => (\n  <Glyph>\n    <rect x=\"1\" y=\"1\" width=\"7\" height=\"8\" fill=\"var(--os9-white)\" stroke=\"currentColor\" strokeWidth=\"1\" transform=\"translate(-.5 -.5)\" />\n    <rect x=\"4\" y=\"4\" width=\"7\" height=\"8\" fill=\"var(--os9-white)\" stroke=\"currentColor\" strokeWidth=\"1\" transform=\"translate(.5 -.5)\" />\n    <rect x=\"6\" y=\"6\" width=\"3\" height=\"1\" fill=\"currentColor\" />\n    <rect x=\"6\" y=\"8\" width=\"3\" height=\"1\" fill=\"currentColor\" />\n  </Glyph>\n)\n\nconst CheckGlyph = () => (\n  <Glyph>\n    <path d=\"M1 6h2v1h1v1h1v-1h1v-1h1v-1h1v-1h1v-1h1v-1h1v2h-1v1h-1v1h-1v1h-1v1h-1v1h-1v1h-2v-1h-1v-1h-1z\" fill=\"currentColor\" />\n  </Glyph>\n)\n\nconst RetryGlyph = () => (\n  <Glyph>\n    <path\n      d=\"M4 2h4v1h-4zM3 3h1v1h-1zM2 4h1v4h-1zM3 8h1v1h-1zM4 9h4v1h-4zM8 8h1v1h-1zM9 5h1v3h-1zM8 0h1v5h-1zM9 1h1v3h-1zM10 2h1v1h-1z\"\n      fill=\"currentColor\"\n    />\n  </Glyph>\n)\n\nconst StopGlyph = () => (\n  <Glyph>\n    <path d=\"M4 0h4v1h1v1h1v1h1v1h1v4h-1v1h-1v1h-1v1h-1v1h-4v-1h-1v-1h-1v-1h-1v-1h-1v-4h1v-1h1v-1h1v-1h1z\" fill=\"var(--os9-black)\" />\n    <path d=\"M4 1h4v1h1v1h1v1h1v4h-1v1h-1v1h-1v1h-4v-1h-1v-1h-1v-1h-1v-4h1v-1h1v-1h1z\" fill=\"#cc0000\" />\n    <rect x=\"3\" y=\"5\" width=\"6\" height=\"2\" fill=\"var(--os9-white)\" />\n  </Glyph>\n)\n\nconst CautionGlyph = () => (\n  <Glyph>\n    <path d=\"M5 0h2v2h1v3h1v3h1v2h1v2h-11v-2h1v-2h1v-3h1v-3h1z\" fill=\"var(--os9-black)\" />\n    <path d=\"M5 2h2v3h1v3h1v2h1v1h-9v-1h1v-2h1v-3h1z\" fill=\"#ffcc00\" />\n    <rect x=\"5\" y=\"4\" width=\"2\" height=\"4\" fill=\"var(--os9-black)\" />\n    <rect x=\"5\" y=\"9\" width=\"2\" height=\"1\" fill=\"var(--os9-black)\" />\n  </Glyph>\n)\n\nconst DownGlyph = () => (\n  <Glyph size={8}>\n    <path d=\"M3 0h2v4h2v1h-1v1h-1v1h-2v-1h-1v-1h-1v-1h2z\" fill=\"currentColor\" />\n  </Glyph>\n)\n\n/* ── thread ────────────────────────────────────────────── */\n\ntype ScrollController = ReturnType<typeof createScrollAreaController>\n\ninterface RetroMessageThreadContextValue {\n  /** Scroll to the newest message and follow it again (call after Send). */\n  scrollToEnd: (behavior?: \"instant\" | \"smooth\") => void\n}\n\nconst ThreadContext = React.createContext<RetroMessageThreadContextValue | null>(null)\n\n/** The enclosing thread's controls, e.g. to scroll to the end after Send. */\nfunction useRetroMessageThread() {\n  const context = React.useContext(ThreadContext)\n  if (!context) throw new Error(\"useRetroMessageThread must be used inside <RetroMessageThread>\")\n  return context\n}\n\ninterface RetroMessageThreadProps extends Omit<React.ComponentProps<\"div\">, \"children\"> {\n  children?: React.ReactNode\n  /**\n   * A reply is streaming. The log is marked aria-busy meanwhile, so screen\n   * readers read the finished reply once instead of every word.\n   */\n  streaming?: boolean\n  /** Accessible name of the log. Default \"Conversation\". */\n  label?: string\n  /** Text of the button that jumps back down. Default \"New messages\". */\n  jumpLabel?: string\n  /** Classes for the inner column (padding, gap, max width). */\n  contentClassName?: string\n  /** Drive the scroll position from outside (`createScrollAreaController()`). */\n  controller?: ScrollController\n}\n\nfunction RetroMessageThread({\n  children,\n  streaming = false,\n  label = \"Conversation\",\n  jumpLabel = \"New messages\",\n  contentClassName,\n  controller: controllerProp,\n  className,\n  ...props\n}: RetroMessageThreadProps) {\n  const [ownController] = React.useState(createScrollAreaController)\n  const controller = controllerProp ?? ownController\n  /* content arrived while the reader was scrolled up */\n  const [hasNew, setHasNew] = React.useState(false)\n\n  const contentRef = React.useCallback(\n    (node: HTMLDivElement | null) => {\n      if (!node) return\n      let height = node.offsetHeight\n      const observer = new ResizeObserver(() => {\n        const next = node.offsetHeight\n        if (next > height && !controller.isSticking() && !controller.isAtEnd()) setHasNew(true)\n        height = next\n      })\n      observer.observe(node)\n      return () => observer.disconnect()\n    },\n    [controller]\n  )\n\n  const context = React.useMemo<RetroMessageThreadContextValue>(\n    () => ({\n      scrollToEnd: (behavior = \"smooth\") => {\n        setHasNew(false)\n        controller.scrollToEnd({ behavior })\n      },\n    }),\n    [controller]\n  )\n\n  return (\n    <ThreadContext.Provider value={context}>\n      <div\n        data-slot=\"retro-message-thread\"\n        className={cn(\"os9-inset relative flex min-h-0 flex-col bg-os9-white\", className)}\n        {...props}\n      >\n        <ScrollAreaPrimitive\n          stickToEnd\n          type=\"always\"\n          controller={controller}\n          className=\"relative min-h-0 flex-1 overflow-hidden\"\n        >\n          <ScrollAreaViewportPrimitive\n            role=\"log\"\n            aria-label={label}\n            aria-live=\"polite\"\n            aria-busy={streaming || undefined}\n            onScroll={() => {\n              if (hasNew && (controller.isSticking() || controller.isAtEnd())) setHasNew(false)\n            }}\n            className=\"h-full w-full pr-[16px] focus-visible:os9-focus-ring\"\n          >\n            <ScrollAreaContentPrimitive>\n              <div\n                ref={contentRef}\n                data-slot=\"retro-message-thread-content\"\n                className={cn(\"flex flex-col gap-4 p-3\", contentClassName)}\n              >\n                {children}\n              </div>\n            </ScrollAreaContentPrimitive>\n          </ScrollAreaViewportPrimitive>\n          <RetroScrollBar orientation=\"vertical\" />\n          {hasNew ? (\n            <div className=\"pointer-events-none absolute inset-x-0 bottom-2 flex justify-center pr-[16px]\">\n              <ScrollAreaScrollToEndPrimitive\n                onClick={() => setHasNew(false)}\n                render={\n                  <RetroBevelButton\n                    bevel=\"small\"\n                    size=\"sm\"\n                    icon={<DownGlyph />}\n                    className={cn(\"pointer-events-auto not-data-visible:hidden\", ENTER)}\n                  />\n                }\n              >\n                {jumpLabel}\n              </ScrollAreaScrollToEndPrimitive>\n            </div>\n          ) : null}\n        </ScrollAreaPrimitive>\n      </div>\n    </ThreadContext.Provider>\n  )\n}\n\n/* ── message ───────────────────────────────────────────── */\n\nconst MessageContext = React.createContext<RetroChatRole>(\"assistant\")\n\ninterface RetroMessageProps extends Omit<React.ComponentProps<\"div\">, \"children\"> {\n  /** The message to render; its parts render in order. */\n  message?: RetroChatMessage\n  /** Who wrote it, for custom content without `message`. Default \"assistant\". */\n  from?: RetroChatRole\n  /**\n   * This message is the reply still arriving. Parts without their own\n   * `state` take the last one as streaming.\n   */\n  streaming?: boolean\n  /** Renders a tool part (e.g. `(part) => <RetroToolCall part={part} />`). */\n  renderTool?: (part: RetroToolPart, index: number) => React.ReactNode\n  /** Replace the assistant's icon, or `false` for none. */\n  avatar?: React.ReactNode | false\n  /** Extra content after the parts, such as RetroMessageActions. */\n  children?: React.ReactNode\n}\n\nfunction RetroMessage({\n  message,\n  from: fromProp,\n  streaming = false,\n  renderTool,\n  avatar,\n  className,\n  children,\n  ...props\n}: RetroMessageProps) {\n  const from = message?.role ?? fromProp ?? \"assistant\"\n  return (\n    <MessageContext.Provider value={from}>\n      <div\n        data-slot=\"retro-message\"\n        data-from={from}\n        data-streaming={streaming || undefined}\n        className={cn(\n          \"group/message flex min-w-0\",\n          from === \"assistant\" && \"items-start gap-2\",\n          ENTER,\n          className\n        )}\n        {...props}\n      >\n        {from === \"assistant\" && avatar !== false ? (\n          <span className=\"flex h-[18px] shrink-0 items-center\">\n            {avatar ?? <RetroAssistantIcon />}\n          </span>\n        ) : null}\n        <div\n          className={cn(\n            \"flex min-w-0 flex-1 flex-col gap-1\",\n            from === \"user\" && \"items-end\",\n            from === \"system\" && \"items-center\"\n          )}\n        >\n          {message ? (\n            <RetroMessageContent>\n              <RetroMessageParts\n                parts={message.parts}\n                streaming={streaming}\n                renderTool={renderTool}\n              />\n            </RetroMessageContent>\n          ) : null}\n          {children}\n        </div>\n      </div>\n    </MessageContext.Provider>\n  )\n}\n\n/** The body of a message: prose for the assistant, a lavender box for the user. */\nfunction RetroMessageContent({ className, ...props }: React.ComponentProps<\"div\">) {\n  const from = React.useContext(MessageContext)\n  return (\n    <div\n      data-slot=\"retro-message-content\"\n      data-from={from}\n      className={cn(\n        \"flex min-w-0 flex-col gap-2 font-[family-name:var(--os9-font-sans)] [overflow-wrap:anywhere]\",\n        from === \"assistant\" && \"w-full text-[12px] leading-[1.5] text-os9-black\",\n        from === \"user\" &&\n          \"max-w-[80%] border border-os9-black bg-os9-lavender px-2 py-1 text-[12px] leading-[1.5] text-os9-black\",\n        from === \"system\" && \"text-center text-[10px] leading-[1.4] text-os9-gray-700\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\n/* ── parts ─────────────────────────────────────────────── */\n\nfunction RetroMessageParts({\n  parts,\n  streaming,\n  renderTool,\n}: {\n  parts: RetroMessagePart[]\n  streaming: boolean\n  renderTool?: (part: RetroToolPart, index: number) => React.ReactNode\n}) {\n  const last = parts.length - 1\n  return parts.map((part, i) => {\n    if (isToolPart(part)) {\n      return (\n        <React.Fragment key={part.toolCallId}>\n          {renderTool ? renderTool(part, i) : <DefaultToolRow part={part} />}\n        </React.Fragment>\n      )\n    }\n    const partStreaming =\n      \"state\" in part && part.state\n        ? part.state === \"streaming\"\n        : streaming && i === last\n    switch (part.type) {\n      case \"text\":\n        return <RetroMessageText key={i} text={part.text} streaming={partStreaming} />\n      case \"reasoning\":\n        return <RetroReasoning key={i} text={part.text} streaming={partStreaming} />\n      case \"file\":\n        return <FilePart key={i} part={part} />\n      default:\n        return null\n    }\n  })\n}\n\n/* fenced code: ``` or ```lang on a line of its own */\nconst FENCE = /^\\s*```\\s*([\\w+#.-]*)\\s*$/\n\ntype Block =\n  | { kind: \"p\"; text: string }\n  | { kind: \"code\"; lang: string; code: string }\n\nfunction parseBlocks(text: string): Block[] {\n  const blocks: Block[] = []\n  let para: string[] = []\n  let code: { lang: string; lines: string[] } | null = null\n  const flush = () => {\n    if (para.length) blocks.push({ kind: \"p\", text: para.join(\"\\n\") })\n    para = []\n  }\n  for (const line of text.split(\"\\n\")) {\n    const fence = FENCE.exec(line)\n    if (code) {\n      if (fence && !fence[1]) {\n        blocks.push({ kind: \"code\", lang: code.lang, code: code.lines.join(\"\\n\") })\n        code = null\n      } else code.lines.push(line)\n    } else if (fence) {\n      flush()\n      code = { lang: fence[1], lines: [] }\n    } else if (line.trim() === \"\") flush()\n    else para.push(line)\n  }\n  flush()\n  /* still streaming inside a fence: show what has arrived */\n  if (code) blocks.push({ kind: \"code\", lang: code.lang, code: code.lines.join(\"\\n\") })\n  return blocks\n}\n\n/* `code` and **bold**; anything else stays plain text */\nfunction inline(text: string) {\n  return text.split(/(`[^`\\n]+`|\\*\\*[^*\\n]+\\*\\*)/g).map((piece, i) =>\n    i % 2 === 0 ? (\n      piece\n    ) : piece.startsWith(\"`\") ? (\n      <code\n        key={i}\n        className=\"bg-os9-gray-200 px-[3px] font-[family-name:var(--os9-font-mono)] text-[11px] outline outline-1 -outline-offset-1 outline-os9-gray-400\"\n      >\n        {piece.slice(1, -1)}\n      </code>\n    ) : (\n      <strong key={i} className=\"font-bold\">\n        {piece.slice(2, -2)}\n      </strong>\n    )\n  )\n}\n\ninterface RetroMessageTextProps extends Omit<React.ComponentProps<\"div\">, \"children\"> {\n  text: string\n  /** More text may still arrive: pace the reveal and show the caret. */\n  streaming?: boolean\n}\n\n/**\n * Message text: paragraphs on blank lines, single newlines kept, `inline\n * code` and fenced blocks in Monaco. No markdown dependency.\n */\nfunction RetroMessageText({ text, streaming = false, className, ref, ...props }: RetroMessageTextProps) {\n  const { shown, phase, ref: paceRef } = useRetroStreamingText(text, streaming)\n  const mergedRef = useMergedRef<HTMLDivElement>(ref, paceRef as React.Ref<HTMLDivElement>)\n  const blocks = parseBlocks(shown)\n  const caret = <RetroCaret phase={phase} />\n  if (blocks.length === 0) blocks.push({ kind: \"p\", text: \"\" })\n  const last = blocks.length - 1\n\n  return (\n    <div\n      ref={mergedRef}\n      data-slot=\"retro-message-text\"\n      className={cn(\"flex flex-col gap-2\", className)}\n      {...props}\n    >\n      {blocks.map((block, i) =>\n        block.kind === \"p\" ? (\n          <p key={i} className=\"whitespace-pre-wrap\">\n            {inline(block.text)}\n            {i === last ? caret : null}\n          </p>\n        ) : (\n          <div key={i} className=\"os9-inset relative min-w-0\">\n            {block.lang ? (\n              <div className=\"border-b border-os9-gray-400 px-2 py-[2px] font-[family-name:var(--os9-font-heading)] text-[10px] text-os9-gray-700\">\n                {block.lang}\n              </div>\n            ) : null}\n            <pre className=\"overflow-x-auto px-2 py-[6px] font-[family-name:var(--os9-font-mono)] text-[10px] leading-[1.5] text-os9-black\">\n              <code>\n                {block.code}\n                {i === last ? caret : null}\n              </code>\n            </pre>\n          </div>\n        )\n      )}\n    </div>\n  )\n}\n\nfunction FilePart({ part }: { part: RetroFilePart }) {\n  if (part.mediaType.startsWith(\"image/\")) {\n    return (\n      // eslint-disable-next-line @next/next/no-img-element\n      <img\n        src={part.url}\n        alt={part.filename ?? \"Attached picture\"}\n        className=\"max-h-48 max-w-full self-start border border-os9-black\"\n      />\n    )\n  }\n  return (\n    <a\n      href={part.url}\n      target=\"_blank\"\n      rel=\"noreferrer\"\n      className=\"flex items-center gap-1 self-start text-[11px] underline focus-visible:os9-focus-ring\"\n    >\n      <RetroIconDocument size=\"sm\" />\n      {part.filename ?? \"Attachment\"}\n    </a>\n  )\n}\n\nconst TOOL_STATE: Record<RetroToolPart[\"state\"], { label: string; icon: React.ReactNode }> = {\n  \"input-streaming\": { label: \"Preparing…\", icon: <RetroWatchIcon size={12} /> },\n  \"input-available\": { label: \"Running…\", icon: <RetroWatchIcon size={12} /> },\n  \"approval-requested\": { label: \"Waiting for you\", icon: <CautionGlyph /> },\n  \"approval-responded\": { label: \"Running…\", icon: <RetroWatchIcon size={12} /> },\n  \"output-available\": { label: \"Done\", icon: <CheckGlyph /> },\n  \"output-error\": { label: \"Error\", icon: <StopGlyph /> },\n  \"output-denied\": { label: \"Not allowed\", icon: <StopGlyph /> },\n}\n\n/** A plain row for tool parts when no `renderTool` is given. */\nfunction DefaultToolRow({ part }: { part: RetroToolPart }) {\n  const { label, icon } = TOOL_STATE[part.state]\n  return (\n    <div data-slot=\"retro-message-tool\" data-state={part.state} className=\"flex items-center gap-[6px]\">\n      {icon}\n      <span className=\"font-[family-name:var(--os9-font-mono)] text-[10px]\">{toolNameOf(part)}</span>\n      <span className=\"text-[10px] text-os9-gray-700\">\n        {part.state === \"output-error\" && part.errorText ? part.errorText : label}\n      </span>\n    </div>\n  )\n}\n\n/* ── actions ───────────────────────────────────────────── */\n\nconst COPIED_MS = 1600\n\ninterface RetroMessageActionsProps extends Omit<React.ComponentProps<\"div\">, \"onCopy\"> {\n  /** The message whose text Copy copies. */\n  message?: RetroChatMessage\n  /** Text to copy instead of the message's. */\n  text?: string\n  /** Called after copying, with the text. */\n  onCopy?: (text: string) => void\n  /** Shows Try Again. */\n  onRetry?: () => void\n  /** Keep them visible rather than showing on hover and focus. */\n  alwaysVisible?: boolean\n}\n\n/**\n * Copy and Try Again under a reply. They show while the pointer is over the\n * message or focus is inside it, and always on touch screens. Pass more\n * buttons as children.\n */\nfunction RetroMessageActions({\n  message,\n  text,\n  onCopy,\n  onRetry,\n  alwaysVisible = false,\n  className,\n  children,\n  ...props\n}: RetroMessageActionsProps) {\n  const [copied, setCopied] = React.useState(false)\n  const timer = React.useRef(0)\n  React.useEffect(() => () => window.clearTimeout(timer.current), [])\n\n  const copy = async () => {\n    const value = text ?? (message ? messageText(message) : \"\")\n    try {\n      await navigator.clipboard.writeText(value)\n    } catch {\n      return\n    }\n    onCopy?.(value)\n    setCopied(true)\n    window.clearTimeout(timer.current)\n    timer.current = window.setTimeout(() => setCopied(false), COPIED_MS)\n  }\n\n  return (\n    <div\n      data-slot=\"retro-message-actions\"\n      data-always-visible={alwaysVisible || undefined}\n      className={cn(\n        \"flex items-center gap-1\",\n        \"opacity-0 transition-opacity duration-120 ease-out\",\n        \"group-hover/message:opacity-100 group-focus-within/message:opacity-100\",\n        \"data-always-visible:opacity-100 [@media(hover:none)]:opacity-100\",\n        \"in-data-[flair=classic]:transition-none motion-reduce:transition-none\",\n        className\n      )}\n      {...props}\n    >\n      <RetroBevelButton bevel=\"small\" size=\"sm\" onClick={copy} icon={copied ? <CheckGlyph /> : <CopyGlyph />}>\n        {/* both labels share one cell, so the button keeps its width */}\n        <span className=\"grid\">\n          <span className={cn(\"col-start-1 row-start-1\", copied && \"invisible\")}>Copy</span>\n          <span aria-hidden={!copied} className={cn(\"col-start-1 row-start-1\", !copied && \"invisible\")}>\n            Copied\n          </span>\n        </span>\n      </RetroBevelButton>\n      <span role=\"status\" className=\"sr-only\">\n        {copied ? \"Copied\" : \"\"}\n      </span>\n      {onRetry ? (\n        <RetroBevelButton bevel=\"small\" size=\"sm\" onClick={onRetry} icon={<RetryGlyph />}>\n          Try Again\n        </RetroBevelButton>\n      ) : null}\n      {children}\n    </div>\n  )\n}\n\nexport {\n  RetroMessageThread,\n  RetroMessage,\n  RetroMessageContent,\n  RetroMessageText,\n  RetroMessageActions,\n  RetroAssistantIcon,\n  useRetroMessageThread,\n}\nexport type {\n  RetroMessageThreadProps,\n  RetroMessageProps,\n  RetroMessageTextProps,\n  RetroMessageActionsProps,\n}\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}