{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "elements-mermaid-diagram",
  "type": "registry:component",
  "title": "Mermaid Diagram",
  "description": "The props-driven Mermaid renderer: zoom overlay, skeleton, and error fallback, no runtime required.",
  "dependencies": [
    "beautiful-mermaid",
    "lucide-react"
  ],
  "files": [
    {
      "type": "registry:component",
      "path": "components/assistant-ui/elements/mermaid-diagram.tsx",
      "sourcePath": "packages/ui/src/components/react/assistant-ui/elements/mermaid-diagram.tsx",
      "content": "\"use client\";\n\nimport { renderMermaidSVG } from \"beautiful-mermaid\";\nimport { Maximize2, Minus, Plus, RotateCcw, X } from \"lucide-react\";\nimport {\n  type FC,\n  type ReactNode,\n  memo,\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"@/lib/utils\";\n\nexport type MermaidDiagramProps = {\n  code: string;\n  className?: string;\n  /** Renders a skeleton instead of the diagram while `true`. */\n  streaming?: boolean;\n};\n\nconst MIN_SCALE = 0.5;\nconst MAX_SCALE = 4;\n\ntype MermaidZoomProps = {\n  svg: string;\n  children: ReactNode;\n};\n\nfunction MermaidZoom({ svg, children }: MermaidZoomProps) {\n  const [isOpen, setIsOpen] = useState(false);\n  const [transform, setTransform] = useState({ x: 0, y: 0, scale: 1 });\n  const triggerRef = useRef<HTMLButtonElement>(null);\n  const closeRef = useRef<HTMLButtonElement>(null);\n  const overlayRef = useRef<HTMLDivElement>(null);\n  const viewportRef = useRef<HTMLDivElement>(null);\n  const drag = useRef<{\n    startX: number;\n    startY: number;\n    originX: number;\n    originY: number;\n  } | null>(null);\n  const transformRef = useRef(transform);\n  transformRef.current = transform;\n\n  const zoomSvg = useMemo(\n    () =>\n      svg\n        .replace(/id=\"([^\"]+)\"/g, 'id=\"$1-zoom\"')\n        .replace(/url\\(#([^)]+)\\)/g, \"url(#$1-zoom)\")\n        .replace(/(href|xlink:href)=\"#([^\"]+)\"/g, '$1=\"#$2-zoom\"'),\n    [svg],\n  );\n\n  const handleClose = useCallback(() => {\n    setIsOpen(false);\n    setTransform({ x: 0, y: 0, scale: 1 });\n    triggerRef.current?.focus();\n  }, []);\n\n  useEffect(() => {\n    if (!isOpen) return;\n    const handleKeyDown = (e: KeyboardEvent) => {\n      if (e.key === \"Escape\") {\n        handleClose();\n        return;\n      }\n      if (e.key !== \"Tab\") return;\n      const focusables = overlayRef.current?.querySelectorAll<HTMLElement>(\n        'a[href], button:not([disabled]), [tabindex]:not([tabindex=\"-1\"])',\n      );\n      const first = focusables?.[0];\n      const last = focusables?.[focusables.length - 1];\n      if (!first || !last) return;\n      if (e.shiftKey && document.activeElement === first) {\n        e.preventDefault();\n        last.focus();\n      } else if (!e.shiftKey && document.activeElement === last) {\n        e.preventDefault();\n        first.focus();\n      }\n    };\n    document.addEventListener(\"keydown\", handleKeyDown);\n    return () => document.removeEventListener(\"keydown\", handleKeyDown);\n  }, [isOpen, handleClose]);\n\n  useEffect(() => {\n    if (!isOpen) return;\n    const originalOverflow = document.body.style.overflow;\n    document.body.style.overflow = \"hidden\";\n    return () => {\n      document.body.style.overflow = originalOverflow;\n    };\n  }, [isOpen]);\n\n  useEffect(() => {\n    if (isOpen) closeRef.current?.focus();\n  }, [isOpen]);\n\n  const zoomBy = useCallback((factor: number, cx?: number, cy?: number) => {\n    setTransform((t) => {\n      const scale = Math.min(MAX_SCALE, Math.max(MIN_SCALE, t.scale * factor));\n      const ratio = scale / t.scale;\n      if (cx === undefined || cy === undefined) {\n        const viewport = viewportRef.current;\n        cx = (viewport?.clientWidth ?? 0) / 2;\n        cy = (viewport?.clientHeight ?? 0) / 2;\n      }\n      return {\n        scale,\n        x: cx - (cx - t.x) * ratio,\n        y: cy - (cy - t.y) * ratio,\n      };\n    });\n  }, []);\n\n  const onWheel = useCallback(\n    (e: React.WheelEvent) => {\n      const viewport = viewportRef.current;\n      if (!viewport) return;\n      const rect = viewport.getBoundingClientRect();\n      zoomBy(\n        Math.exp(-e.deltaY * 0.0015),\n        e.clientX - rect.left,\n        e.clientY - rect.top,\n      );\n    },\n    [zoomBy],\n  );\n\n  const onPointerDown = useCallback((e: React.PointerEvent) => {\n    e.currentTarget.setPointerCapture(e.pointerId);\n    const t = transformRef.current;\n    drag.current = {\n      startX: e.clientX,\n      startY: e.clientY,\n      originX: t.x,\n      originY: t.y,\n    };\n  }, []);\n\n  const onPointerMove = useCallback((e: React.PointerEvent) => {\n    const d = drag.current;\n    if (!d) return;\n    setTransform((t) => ({\n      ...t,\n      x: d.originX + e.clientX - d.startX,\n      y: d.originY + e.clientY - d.startY,\n    }));\n  }, []);\n\n  const onPointerUp = useCallback(() => {\n    drag.current = null;\n  }, []);\n\n  return (\n    <div\n      data-slot=\"mermaid-zoom-wrap\"\n      className=\"aui-mermaid-zoom-wrap group/mermaid relative\"\n    >\n      {children}\n      <button\n        ref={triggerRef}\n        type=\"button\"\n        data-slot=\"mermaid-zoom-trigger\"\n        aria-label=\"Expand diagram\"\n        onClick={() => setIsOpen(true)}\n        className=\"aui-mermaid-zoom-trigger text-muted-foreground hover:text-foreground hover:border-muted-foreground/70 border-border bg-background absolute top-2 right-2 cursor-pointer rounded-md border p-1.5 opacity-0 transition group-hover/mermaid:opacity-100 focus-visible:opacity-100\"\n      >\n        <Maximize2 className=\"size-3.5\" />\n      </button>\n      {isOpen &&\n        createPortal(\n          <div\n            ref={overlayRef}\n            data-slot=\"mermaid-zoom-overlay\"\n            role=\"dialog\"\n            aria-modal=\"true\"\n            aria-label=\"Diagram\"\n            className=\"aui-mermaid-zoom-overlay fade-in animate-in bg-background fixed inset-0 z-50 duration-200\"\n          >\n            <div\n              ref={viewportRef}\n              className=\"aui-mermaid-zoom-viewport h-full w-full cursor-grab touch-none overflow-hidden active:cursor-grabbing\"\n              onWheel={onWheel}\n              onPointerDown={onPointerDown}\n              onPointerMove={onPointerMove}\n              onPointerUp={onPointerUp}\n              onPointerCancel={onPointerUp}\n            >\n              <div\n                data-slot=\"mermaid-zoom-content\"\n                className=\"aui-mermaid-zoom-content flex h-full w-full items-center justify-center [&_svg]:max-h-[80vh] [&_svg]:max-w-[90vw]\"\n                style={{\n                  transform: `translate(${transform.x}px, ${transform.y}px) scale(${transform.scale})`,\n                  transformOrigin: \"0 0\",\n                }}\n                dangerouslySetInnerHTML={{ __html: zoomSvg }}\n              />\n            </div>\n            <div\n              data-slot=\"mermaid-zoom-toolbar\"\n              className=\"aui-mermaid-zoom-toolbar border-border bg-background absolute top-4 right-4 flex items-center gap-1 rounded-lg border p-1\"\n            >\n              <button\n                type=\"button\"\n                aria-label=\"Zoom in\"\n                onClick={() => zoomBy(1.25)}\n                className=\"text-muted-foreground hover:text-foreground cursor-pointer rounded-sm p-1.5\"\n              >\n                <Plus className=\"size-4\" />\n              </button>\n              <button\n                type=\"button\"\n                aria-label=\"Zoom out\"\n                onClick={() => zoomBy(0.8)}\n                className=\"text-muted-foreground hover:text-foreground cursor-pointer rounded-sm p-1.5\"\n              >\n                <Minus className=\"size-4\" />\n              </button>\n              <button\n                type=\"button\"\n                aria-label=\"Reset zoom\"\n                onClick={() => setTransform({ x: 0, y: 0, scale: 1 })}\n                className=\"text-muted-foreground hover:text-foreground cursor-pointer rounded-sm p-1.5\"\n              >\n                <RotateCcw className=\"size-4\" />\n              </button>\n              <button\n                ref={closeRef}\n                type=\"button\"\n                aria-label=\"Close\"\n                onClick={handleClose}\n                className=\"text-muted-foreground hover:text-foreground cursor-pointer rounded-sm p-1.5\"\n              >\n                <X className=\"size-4\" />\n              </button>\n            </div>\n          </div>,\n          document.body,\n        )}\n    </div>\n  );\n}\n\nconst MermaidDiagramImpl: FC<MermaidDiagramProps> = ({\n  code,\n  className,\n  streaming = false,\n}) => {\n  const result = useMemo(() => {\n    if (streaming) return null;\n    try {\n      return {\n        svg: renderMermaidSVG(code, {\n          bg: \"var(--background)\",\n          fg: \"var(--foreground)\",\n          muted: \"var(--muted-foreground)\",\n          border: \"var(--border)\",\n          accent: \"var(--foreground)\",\n          transparent: true,\n        }),\n        error: null,\n      };\n    } catch (err) {\n      return {\n        svg: null,\n        error: err instanceof Error ? err : new Error(String(err)),\n      };\n    }\n  }, [streaming, code]);\n\n  if (!result) {\n    return (\n      <div\n        data-slot=\"mermaid-skeleton\"\n        aria-label=\"Rendering diagram\"\n        className={cn(\n          \"aui-mermaid-skeleton bg-muted flex h-32 animate-pulse items-center justify-center gap-3 rounded-b-lg p-4\",\n          className,\n        )}\n      >\n        <div className=\"bg-muted-foreground/20 h-8 w-20 rounded-md\" />\n        <div className=\"bg-muted-foreground/20 h-px w-10\" />\n        <div className=\"bg-muted-foreground/20 h-8 w-20 rounded-md\" />\n        <div className=\"bg-muted-foreground/20 h-px w-10\" />\n        <div className=\"bg-muted-foreground/20 h-8 w-20 rounded-md\" />\n      </div>\n    );\n  }\n\n  if (result.error) {\n    return (\n      <div\n        data-slot=\"mermaid-fallback\"\n        className={cn(\n          \"aui-mermaid-fallback bg-muted/75 rounded-b-lg\",\n          className,\n        )}\n      >\n        <pre className=\"overflow-x-auto p-4 text-sm\">{code.trim()}</pre>\n        <p className=\"text-muted-foreground border-border border-t px-4 py-1.5 text-xs\">\n          diagram could not be rendered\n        </p>\n      </div>\n    );\n  }\n\n  return (\n    <MermaidZoom svg={result.svg}>\n      <div\n        data-slot=\"mermaid-diagram\"\n        className={cn(\n          \"aui-mermaid-diagram bg-muted overflow-x-auto rounded-b-lg p-2 [&_svg]:mx-auto [&_svg]:h-auto [&_svg]:max-w-full\",\n          className,\n        )}\n        dangerouslySetInnerHTML={{ __html: result.svg }}\n      />\n    </MermaidZoom>\n  );\n};\n\nconst MermaidDiagram = memo(\n  MermaidDiagramImpl,\n) as unknown as FC<MermaidDiagramProps> & {\n  Zoom: typeof MermaidZoom;\n};\n\nMermaidDiagram.displayName = \"MermaidDiagram\";\nMermaidDiagram.Zoom = MermaidZoom;\n\nexport { MermaidDiagram, MermaidZoom };\n"
    }
  ]
}