{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"generative-ui","type":"registry:ui","title":"Generative UI","description":"Render a React component from a tool result instead of text — a skeleton in the widget's own shape, and a text fallback when the client has no renderer.","author":"Scrim UI (https://scrimui.dev)","categories":["tool-calls"],"docs":"https://scrimui.dev/components/generative-ui","dependencies":[],"registryDependencies":[],"files":[{"path":"src/showcase/generative-ui/generative-ui.tsx","content":"\"use client\";\n\nimport * as React from \"react\";\n\n/* ------------------------------------------------------------------ */\n/* Types                                                               */\n/* ------------------------------------------------------------------ */\n\n/**\n * `unsupported` is not an error state. A generative UI stream carries the\n * name of a widget the server decided to render, and a client is always one\n * deploy behind some of those names — a tool added this week, a lazy chunk\n * that failed, an older mobile build. The model still returned usable\n * content; the only thing missing is the renderer. Treating that as a failure\n * throws away an answer the user could have read.\n */\nexport type GenerativeState = \"streaming\" | \"ready\" | \"unsupported\";\n\nexport type GenerativeUiProps = {\n  /** Tool the widget was rendered from. Shown as attribution. */\n  tool: string;\n  state?: GenerativeState;\n  /** The widget. Rendered only once the tool result is complete. */\n  children?: React.ReactNode;\n  /**\n   * Placeholder for `streaming`. Pass the widget's own shape rather than a\n   * spinner — the layout is known before the data is, so there is no reason\n   * to make the reader watch the card resize when it arrives.\n   */\n  skeleton?: React.ReactNode;\n  /** Readable stand-in for `unsupported`. Prose, not an error code. */\n  fallback?: React.ReactNode;\n  /** Raw tool result behind the Data toggle. Omit to hide the toggle. */\n  data?: string;\n  defaultDataOpen?: boolean;\n  className?: string;\n};\n\n/* ------------------------------------------------------------------ */\n/* Icons                                                               */\n/* ------------------------------------------------------------------ */\n\nfunction SparkIcon(props: React.SVGProps<SVGSVGElement>) {\n  return (\n    <svg viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"12\" height=\"12\" {...props}>\n      <path d=\"M12 2.5 13.7 8 19 9.7 13.7 11.4 12 16.9 10.3 11.4 5 9.7 10.3 8ZM18.5 15l.8 2.4 2.4.8-2.4.8-.8 2.4-.8-2.4-2.4-.8 2.4-.8Z\" />\n    </svg>\n  );\n}\n\nfunction ChevronIcon(props: React.SVGProps<SVGSVGElement>) {\n  return (\n    <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" width=\"12\" height=\"12\" {...props}>\n      <path d=\"m6 9 6 6 6-6\" />\n    </svg>\n  );\n}\n\nfunction TextIcon(props: React.SVGProps<SVGSVGElement>) {\n  return (\n    <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" width=\"14\" height=\"14\" {...props}>\n      <path d=\"M4 6h16M4 12h16M4 18h10\" />\n    </svg>\n  );\n}\n\n/* ------------------------------------------------------------------ */\n/* Default skeleton                                                    */\n/* ------------------------------------------------------------------ */\n\n/**\n * Only a fallback for the `skeleton` prop. Generic bars are the thing this\n * component exists to avoid, so a real integration should pass the widget's\n * own outline instead.\n */\nfunction DefaultSkeleton() {\n  return (\n    <div className=\"animate-pulse space-y-2.5\">\n      <div className=\"h-3 w-1/3 rounded-full bg-zinc-200 dark:bg-zinc-800\" />\n      <div className=\"h-8 w-2/3 rounded-lg bg-zinc-200 dark:bg-zinc-800\" />\n      <div className=\"h-3 w-1/2 rounded-full bg-zinc-200 dark:bg-zinc-800\" />\n    </div>\n  );\n}\n\n/* ------------------------------------------------------------------ */\n/* GenerativeUi                                                        */\n/* ------------------------------------------------------------------ */\n\nexport function GenerativeUi({\n  tool,\n  state = \"ready\",\n  children,\n  skeleton,\n  fallback,\n  data,\n  defaultDataOpen = false,\n  className = \"\",\n}: GenerativeUiProps) {\n  const [dataOpen, setDataOpen] = React.useState(defaultDataOpen);\n  const hasData = (data?.length ?? 0) > 0;\n\n  return (\n    <div\n      className={`overflow-hidden rounded-xl border bg-white transition-colors dark:bg-zinc-900 ${\n        state === \"unsupported\"\n          ? \"border-amber-200 dark:border-amber-900/60\"\n          : \"border-zinc-200 dark:border-zinc-800\"\n      } ${className}`}\n    >\n      {/* The widget leads. A tool call is a process the reader is waiting on,\n          so that component puts its status bar on top; this one is a result\n          they are reading, and pushing it below a status bar would make the\n          chrome look like the content. */}\n      <div className=\"px-3.5 py-3\">\n        {state === \"streaming\" && (\n          /* aria-busy rather than a live region: the skeleton is decorative,\n             and announcing every bar as it settles is noise. The completed\n             widget is what should be read out, and it announces itself. */\n          <div aria-busy=\"true\">{skeleton ?? <DefaultSkeleton />}</div>\n        )}\n\n        {state === \"ready\" && children}\n\n        {state === \"unsupported\" && (\n          <div className=\"flex gap-2.5\">\n            <span className=\"mt-px shrink-0 text-amber-600 dark:text-amber-500\">\n              <TextIcon />\n            </span>\n            <div className=\"min-w-0 text-sm leading-6 text-zinc-700 dark:text-zinc-300\">\n              {fallback ?? `This app cannot display the ${tool} result yet.`}\n            </div>\n          </div>\n        )}\n      </div>\n\n      {/* Attribution.\n\n          Generative UI puts model output in the same visual language as the\n          app's own interface, which is exactly what makes it worth building\n          and exactly what makes it worth labelling. The footer is the answer\n          to \"did a person build this card or did a model fill it in?\" */}\n      <div className=\"flex items-center gap-2 border-t border-zinc-100 bg-zinc-50/60 px-3.5 py-2 dark:border-zinc-800 dark:bg-zinc-800/30\">\n        <span className=\"shrink-0 text-zinc-400 dark:text-zinc-500\">\n          <SparkIcon />\n        </span>\n        <span className=\"min-w-0 truncate font-mono text-[11px] text-zinc-500 dark:text-zinc-400\">\n          {tool}\n        </span>\n        {state === \"streaming\" && (\n          <span className=\"shrink-0 text-[11px] text-zinc-400 dark:text-zinc-500\">generating…</span>\n        )}\n        {state === \"unsupported\" && (\n          <span className=\"shrink-0 text-[11px] text-amber-600 dark:text-amber-500\">\n            no renderer\n          </span>\n        )}\n        {hasData && (\n          <button\n            type=\"button\"\n            onClick={() => setDataOpen((v) => !v)}\n            aria-expanded={dataOpen}\n            /* min-h-6 keeps the tap target at the 24px minimum; at this text\n               size the padding alone lands around 20px. */\n            className=\"ml-auto inline-flex min-h-6 shrink-0 items-center gap-1 rounded-md px-1.5 text-[11px] text-zinc-500 transition-colors hover:bg-zinc-200/60 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-700/60 dark:hover:text-zinc-200\"\n          >\n            Data\n            <ChevronIcon className={dataOpen ? \"rotate-180\" : \"\"} />\n          </button>\n        )}\n      </div>\n\n      {hasData && dataOpen && (\n        <pre className=\"max-h-56 overflow-auto border-t border-zinc-100 bg-zinc-50 p-3 font-mono text-xs leading-5 text-zinc-700 dark:border-zinc-800 dark:bg-zinc-800/60 dark:text-zinc-300\">\n          {data}\n        </pre>\n      )}\n    </div>\n  );\n}\n","type":"registry:ui","target":"components/ui/generative-ui.tsx"}]}