{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "media-router",
  "title": "Media Router",
  "description": "MIME type-based output dispatcher that automatically selects the best renderer for Jupyter outputs. Supports custom renderers and priority ordering for platform-specific MIME types.",
  "registryDependencies": [
    "@nteract/media-provider",
    "@nteract/ansi-output",
    "@nteract/markdown-output",
    "@nteract/html-output",
    "@nteract/image-output",
    "@nteract/svg-output",
    "@nteract/json-output"
  ],
  "files": [
    {
      "path": "registry/outputs/media-router.tsx",
      "content": "import { lazy, type ReactNode, Suspense } from \"react\";\nimport { useMediaContext } from \"./media-provider\";\n\n// Lazy load built-in output components for better bundle splitting\nconst AnsiOutput = lazy(() =>\n  import(\"./ansi-output\").then((m) => ({ default: m.AnsiOutput })),\n);\nconst MarkdownOutput = lazy(() =>\n  import(\"./markdown-output\").then((m) => ({ default: m.MarkdownOutput })),\n);\nconst HtmlOutput = lazy(() =>\n  import(\"./html-output\").then((m) => ({ default: m.HtmlOutput })),\n);\nconst ImageOutput = lazy(() =>\n  import(\"./image-output\").then((m) => ({ default: m.ImageOutput })),\n);\nconst SvgOutput = lazy(() =>\n  import(\"./svg-output\").then((m) => ({ default: m.SvgOutput })),\n);\nconst JsonOutput = lazy(() =>\n  import(\"./json-output\").then((m) => ({ default: m.JsonOutput })),\n);\n\n/**\n * Check if the current window is inside an iframe\n */\nfunction isInIframe(): boolean {\n  if (typeof window === \"undefined\") return false;\n  try {\n    return window.self !== window.top;\n  } catch {\n    // If we can't access window.top due to cross-origin restrictions,\n    // we're definitely in an iframe\n    return true;\n  }\n}\n\n/**\n * Default MIME type priority order for rendering.\n * Higher priority types are preferred when multiple are available.\n * Platforms can override this with the `priority` prop.\n */\nexport const DEFAULT_PRIORITY = [\n  // Rich formats first\n  \"application/vnd.jupyter.widget-view+json\",\n  \"application/vnd.plotly.v1+json\",\n  \"application/vnd.vegalite.v5+json\",\n  \"application/vnd.vegalite.v4+json\",\n  \"application/vnd.vegalite.v3+json\",\n  \"application/vnd.vega.v5+json\",\n  \"application/vnd.vega.v4+json\",\n  \"application/geo+json\",\n  // HTML and markdown\n  \"text/html\",\n  \"text/markdown\",\n  // Images\n  \"image/svg+xml\",\n  \"image/png\",\n  \"image/jpeg\",\n  \"image/gif\",\n  \"image/webp\",\n  // Structured data\n  \"application/json\",\n  // Plain text (fallback)\n  \"text/plain\",\n] as const;\n\ntype MimeType = (typeof DEFAULT_PRIORITY)[number] | string;\n\ninterface MediaData {\n  [mimeType: string]: unknown;\n}\n\ninterface MediaMetadata {\n  [mimeType: string]:\n    | {\n        width?: number;\n        height?: number;\n        [key: string]: unknown;\n      }\n    | undefined;\n}\n\n/**\n * Props passed to custom renderer functions.\n */\nexport interface RendererProps {\n  data: unknown;\n  metadata: Record<string, unknown>;\n  mimeType: string;\n  className?: string;\n}\n\n/**\n * Custom renderer function type.\n */\nexport type CustomRenderer = (props: RendererProps) => ReactNode;\n\ninterface MediaRouterProps {\n  /**\n   * Output data object mapping MIME types to content.\n   * e.g., { \"text/plain\": \"Hello\", \"text/html\": \"<b>Hello</b>\" }\n   */\n  data: MediaData;\n  /**\n   * Output metadata object mapping MIME types to their metadata.\n   * e.g., { \"image/png\": { width: 400, height: 300 } }\n   */\n  metadata?: MediaMetadata;\n  /**\n   * Custom MIME type priority order. Types listed first are preferred.\n   * Defaults to DEFAULT_PRIORITY. Your custom types should come first,\n   * followed by spreading DEFAULT_PRIORITY for fallback.\n   *\n   * @example\n   * ```tsx\n   * priority={[\"application/vnd.plotly.v1+json\", ...DEFAULT_PRIORITY]}\n   * ```\n   */\n  priority?: readonly string[];\n  /**\n   * Custom renderers keyed by MIME type. Use this to handle MIME types\n   * not supported by the built-in renderers, or to override built-ins.\n   *\n   * @example\n   * ```tsx\n   * renderers={{\n   *   \"application/vnd.plotly.v1+json\": ({ data }) => <PlotlyChart data={data} />,\n   *   \"application/geo+json\": ({ data }) => <GeoJsonMap data={data} />,\n   * }}\n   * ```\n   */\n  renderers?: Record<string, CustomRenderer>;\n  /**\n   * Custom fallback component when no supported MIME type is found.\n   */\n  fallback?: ReactNode;\n  /**\n   * Loading component shown while lazy-loading output components.\n   */\n  loading?: ReactNode;\n  /**\n   * Additional CSS classes passed to the rendered output.\n   */\n  className?: string;\n}\n\n/**\n * Select the best MIME type from available data based on priority.\n */\nfunction selectMimeType(\n  data: MediaData,\n  priority: readonly string[],\n): MimeType | null {\n  const availableTypes = Object.keys(data);\n\n  // Check priority list first\n  for (const mimeType of priority) {\n    if (availableTypes.includes(mimeType) && data[mimeType] != null) {\n      return mimeType;\n    }\n  }\n\n  // Fall back to first available type\n  const firstAvailable = availableTypes.find((type) => data[type] != null);\n  return firstAvailable || null;\n}\n\n/**\n * Default loading spinner\n */\nfunction DefaultLoading() {\n  return (\n    <div className=\"flex items-center justify-center py-4 text-gray-400\">\n      <svg\n        className=\"h-5 w-5 animate-spin\"\n        xmlns=\"http://www.w3.org/2000/svg\"\n        fill=\"none\"\n        viewBox=\"0 0 24 24\"\n      >\n        <circle\n          className=\"opacity-25\"\n          cx=\"12\"\n          cy=\"12\"\n          r=\"10\"\n          stroke=\"currentColor\"\n          strokeWidth=\"4\"\n        />\n        <path\n          className=\"opacity-75\"\n          fill=\"currentColor\"\n          d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"\n        />\n      </svg>\n    </div>\n  );\n}\n\n/**\n * MediaRouter component for rendering Jupyter outputs based on MIME type.\n *\n * Automatically selects the best available renderer for the output data,\n * following Jupyter's MIME type priority conventions. Supports custom\n * renderers and priority ordering for platform-specific MIME types.\n *\n * @example\n * ```tsx\n * // Basic usage\n * <MediaRouter\n *   data={{\n *     \"text/plain\": \"Hello, World!\",\n *     \"text/html\": \"<b>Hello, World!</b>\"\n *   }}\n * />\n *\n * // With custom renderers\n * <MediaRouter\n *   data={output.data}\n *   metadata={output.metadata}\n *   priority={[\"application/vnd.plotly.v1+json\", ...DEFAULT_PRIORITY]}\n *   renderers={{\n *     \"application/vnd.plotly.v1+json\": ({ data }) => <PlotlyChart data={data} />,\n *   }}\n * />\n * ```\n */\nexport function MediaRouter({\n  data,\n  metadata = {},\n  priority: priorityProp,\n  renderers: renderersProp,\n  fallback,\n  loading,\n  className = \"\",\n}: MediaRouterProps) {\n  const ctx = useMediaContext();\n\n  // Props override context, context overrides built-in defaults\n  const priority = priorityProp ?? ctx?.priority ?? DEFAULT_PRIORITY;\n  const renderers = renderersProp ?? ctx?.renderers ?? {};\n\n  const mimeType = selectMimeType(data, priority);\n\n  if (!mimeType) {\n    return (\n      <div data-slot=\"media-router\">\n        {fallback ? (\n          fallback\n        ) : (\n          <div className=\"py-2 text-sm text-gray-500\">\n            No displayable output\n          </div>\n        )}\n      </div>\n    );\n  }\n\n  const content = data[mimeType];\n  const mimeMetadata = (metadata[mimeType] || {}) as Record<string, unknown>;\n  const loadingComponent = loading || <DefaultLoading />;\n\n  // Check for custom renderer first\n  if (renderers[mimeType]) {\n    const customRenderer = renderers[mimeType];\n    return (\n      <div data-slot=\"media-router\" data-mime-type={mimeType}>\n        <Suspense fallback={loadingComponent}>\n          {customRenderer({\n            data: content,\n            metadata: mimeMetadata,\n            mimeType,\n            className,\n          })}\n        </Suspense>\n      </div>\n    );\n  }\n\n  const renderBuiltIn = () => {\n    // ISOLATION GUARD: These MIME types require iframe isolation for security.\n    // If we're already in an iframe, we can render them safely.\n    // If we're in the main DOM, block rendering and warn in development.\n    const needsIsolation =\n      mimeType === \"text/markdown\" ||\n      mimeType === \"text/html\" ||\n      mimeType === \"image/svg+xml\";\n\n    if (needsIsolation && !isInIframe()) {\n      if (process.env.NODE_ENV === \"development\") {\n        console.warn(\n          `MediaRouter: \"${mimeType}\" requires iframe isolation. ` +\n            `Use OutputArea with isolated=\"auto\" or IsolatedFrame directly.`,\n        );\n      }\n      return null;\n    }\n\n    // Text/Markdown (only renders when in iframe)\n    if (mimeType === \"text/markdown\") {\n      return <MarkdownOutput content={String(content)} className={className} />;\n    }\n\n    // HTML (only renders when in iframe)\n    if (mimeType === \"text/html\") {\n      return <HtmlOutput content={String(content)} className={className} />;\n    }\n\n    // SVG (only renders when in iframe)\n    if (mimeType === \"image/svg+xml\") {\n      return <SvgOutput data={String(content)} className={className} />;\n    }\n\n    // Images (not SVG)\n    if (mimeType.startsWith(\"image/\")) {\n      const imageType = mimeType as\n        | \"image/png\"\n        | \"image/jpeg\"\n        | \"image/gif\"\n        | \"image/webp\";\n      return (\n        <ImageOutput\n          data={String(content)}\n          mediaType={imageType}\n          width={mimeMetadata.width as number | undefined}\n          height={mimeMetadata.height as number | undefined}\n          className={className}\n        />\n      );\n    }\n\n    // JSON and structured data (but not custom +json types without a renderer)\n    if (mimeType === \"application/json\") {\n      return (\n        <JsonOutput\n          data={content}\n          collapsed={mimeMetadata.collapsed as boolean | number | undefined}\n          className={className}\n        />\n      );\n    }\n\n    // Plain text (may contain ANSI)\n    if (mimeType === \"text/plain\") {\n      return <AnsiOutput className={className}>{String(content)}</AnsiOutput>;\n    }\n\n    // Widget view JSON - when rendered in-DOM without a widget renderer,\n    // show a helpful message instead of raw JSON. This typically happens\n    // when a widget is displayed inside an Output widget.\n    if (mimeType === \"application/vnd.jupyter.widget-view+json\") {\n      return (\n        <div className=\"py-2 px-3 text-sm text-muted-foreground bg-muted/50 rounded border border-border\">\n          <span className=\"font-medium\">Nested widget</span>\n          <span className=\"mx-1\">·</span>\n          <span>Widgets inside Output widgets are not yet supported</span>\n        </div>\n      );\n    }\n\n    // Unknown +json types without custom renderer - show as JSON\n    if (mimeType.includes(\"+json\")) {\n      return (\n        <JsonOutput\n          data={content}\n          collapsed={mimeMetadata.collapsed as boolean | number | undefined}\n          className={className}\n        />\n      );\n    }\n\n    // Fallback: render as plain text\n    return <AnsiOutput className={className}>{String(content)}</AnsiOutput>;\n  };\n\n  return (\n    <div data-slot=\"media-router\" data-mime-type={mimeType}>\n      <Suspense fallback={loadingComponent}>{renderBuiltIn()}</Suspense>\n    </div>\n  );\n}\n\n/**\n * Get the selected MIME type for debugging/display purposes.\n */\nexport function getSelectedMimeType(\n  data: MediaData,\n  priority: readonly string[] = DEFAULT_PRIORITY,\n): string | null {\n  return selectMimeType(data, priority);\n}\n",
      "type": "registry:component",
      "target": "components/outputs/media-router.tsx"
    }
  ],
  "type": "registry:component"
}