{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "mindmap",
  "title": "MindMap",
  "description": "A Mind Elixir-powered mind map component with theme-aware styling, controls, and presentation-first defaults.",
  "dependencies": [
    "mind-elixir",
    "lucide-react",
    "@mind-elixir/export-mindmap"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "src/registry/mindmap.tsx",
      "content": "\"use client\";\r\n\r\nimport \"mind-elixir/style.css\";\r\nimport {\r\n  createContext,\r\n  useContext,\r\n  useEffect,\r\n  useId,\r\n  useRef,\r\n  useState,\r\n  forwardRef,\r\n  useImperativeHandle,\r\n  type ReactNode,\r\n} from \"react\";\r\nimport {\r\n  Minus,\r\n  Plus,\r\n  Download,\r\n  Loader2,\r\n  Maximize,\r\n  ScanSearch,\r\n} from \"lucide-react\";\r\n\r\nimport { cn } from \"@/lib/utils\";\r\nimport {\r\n  type MindElixirInstance,\r\n  type MindElixirData,\r\n  type NodeObj,\r\n  type Options,\r\n  type Theme as MindElixirTheme,\r\n} from \"mind-elixir\";\r\nimport MindElixir from \"mind-elixir\";\r\nimport { plaintextToMindElixir } from \"mind-elixir/plaintextConverter\";\r\nimport { exportImageBlob } from \"@mind-elixir/export-mindmap\";\r\n\r\n/** Custom markdown parser — same signature as Mind Elixir `Options.markdown`. */\r\nexport type MindMapMarkdownParser = NonNullable<Options[\"markdown\"]>;\r\n\r\n// Check document class for theme (works with next-themes, etc.)\r\nfunction getDocumentTheme(): Theme | null {\r\n  if (typeof document === \"undefined\") return null;\r\n  if (document.documentElement.classList.contains(\"dark\")) return \"dark\";\r\n  if (document.documentElement.classList.contains(\"light\")) return \"light\";\r\n  return null;\r\n}\r\n\r\n// Get system preference\r\nfunction getSystemTheme(): Theme {\r\n  if (typeof window === \"undefined\") return \"light\";\r\n  return window.matchMedia(\"(prefers-color-scheme: dark)\").matches\r\n    ? \"dark\"\r\n    : \"light\";\r\n}\r\n\r\nfunction useResolvedTheme(themeProp?: \"light\" | \"dark\"): \"light\" | \"dark\" {\r\n  const [detectedTheme, setDetectedTheme] = useState<\"light\" | \"dark\">(\r\n    () => getDocumentTheme() ?? getSystemTheme(),\r\n  );\r\n\r\n  useEffect(() => {\r\n    if (themeProp) return; // Skip detection if theme is provided via prop\r\n\r\n    // Watch for document class changes (e.g., next-themes toggling dark class)\r\n    const observer = new MutationObserver(() => {\r\n      const docTheme = getDocumentTheme();\r\n      if (docTheme) {\r\n        setDetectedTheme(docTheme);\r\n      }\r\n    });\r\n    observer.observe(document.documentElement, {\r\n      attributes: true,\r\n      attributeFilter: [\"class\"],\r\n    });\r\n\r\n    // Also watch for system preference changes\r\n    const mediaQuery = window.matchMedia(\"(prefers-color-scheme: dark)\");\r\n    const handleSystemChange = (e: MediaQueryListEvent) => {\r\n      // Only use system preference if no document class is set\r\n      if (!getDocumentTheme()) {\r\n        setDetectedTheme(e.matches ? \"dark\" : \"light\");\r\n      }\r\n    };\r\n    mediaQuery.addEventListener(\"change\", handleSystemChange);\r\n\r\n    return () => {\r\n      observer.disconnect();\r\n      mediaQuery.removeEventListener(\"change\", handleSystemChange);\r\n    };\r\n  }, [themeProp]);\r\n\r\n  return themeProp ?? detectedTheme;\r\n}\r\n\r\ntype Theme = \"light\" | \"dark\";\r\n\r\n// Context for MindMap\r\ninterface MindMapContextValue {\r\n  mind: MindElixirInstance | null;\r\n  isLoaded: boolean;\r\n}\r\n\r\nconst MindMapContext = createContext<MindMapContextValue | null>(null);\r\n\r\nexport function useMindMap() {\r\n  const context = useContext(MindMapContext);\r\n  if (!context) {\r\n    throw new Error(\"useMindMap must be used within a MindMap component\");\r\n  }\r\n  return context;\r\n}\r\n\r\n// MindMap Props\r\ntype MindMapData = MindElixirData;\r\n\r\n/**\r\n * Accepted data input for the MindMap component.\r\n * Either a `MindElixirData` object or a Mind Elixir plaintext string\r\n * (indentation-based outline that is parsed via `plaintextToMindElixir`).\r\n */\r\nexport type MindMapInput = MindElixirData | string;\r\n\r\n/** Normalize accepted input (object or plaintext string) to MindElixirData. */\r\nfunction toData(input: MindMapInput | undefined): MindElixirData | undefined {\r\n  if (input == null) return undefined;\r\n  if (typeof input === \"string\") return plaintextToMindElixir(input);\r\n  return input;\r\n}\r\n\r\n// Ref type to expose MindElixir instance to parent components\r\nexport interface MindMapRef {\r\n  instance: MindElixirInstance | null;\r\n}\r\n\r\ninterface MindMapProps {\r\n  children?: ReactNode;\r\n  /**\r\n   * Map data. Accepts a `MindElixirData` object or a Mind Elixir plaintext\r\n   * string (indentation-based outline).\r\n   */\r\n  data?: MindMapInput;\r\n  className?: string;\r\n  direction?: 0 | 1 | 2;\r\n  contextMenu?: boolean;\r\n  keypress?: boolean;\r\n  locale?: \"en\" | \"zh_CN\" | \"zh_TW\" | \"ja\" | \"pt\";\r\n  overflowHidden?: boolean;\r\n  theme?: \"dark\" | \"light\";\r\n  monochrome?: boolean;\r\n  fit?: boolean;\r\n  readonly?: boolean;\r\n  /**\r\n   * Compact layout with tighter node spacing.\r\n   * Useful for dense presentation maps.\r\n   */\r\n  compact?: boolean;\r\n  /**\r\n   * Custom markdown parser. When provided, node topics (and arrow/summary\r\n   * labels) are passed through this function before rendering as HTML.\r\n   * Bring your own parser (e.g. marked, markdown-it) or a small custom one.\r\n   */\r\n  markdown?: MindMapMarkdownParser;\r\n  /**\r\n   * Rewrite remote node image URLs so they can be captured when exporting\r\n   * the map (avoids CORS failures during image generation). Not needed for\r\n   * normal on-screen viewing alone.\r\n   */\r\n  imageProxy?: (url: string) => string;\r\n  onChange?: (data: MindMapData, operation: unknown) => void;\r\n  onOperation?: (operation: unknown) => void;\r\n  onSelectNodes?: (nodeObj: NodeObj[]) => void;\r\n  loader?: ReactNode;\r\n}\r\n\r\nfunction DefaultLoader() {\r\n  return (\r\n    <div className=\"absolute inset-0 flex items-center justify-center bg-background/80 backdrop-blur-sm\">\r\n      <Loader2 className=\"size-8 animate-spin text-muted-foreground\" />\r\n    </div>\r\n  );\r\n}\r\n\r\n// Common spacing and layout configuration\r\nconst commonSpacing = {\r\n  \"--node-gap-x\": \"48px\",\r\n  \"--node-gap-y\": \"16px\",\r\n  \"--main-gap-x\": \"24px\",\r\n  \"--main-gap-y\": \"32px\",\r\n  \"--root-radius\": \"0.625rem\",\r\n  \"--main-radius\": \"0.5rem\",\r\n  \"--topic-padding\": \"8px 16px\",\r\n  \"--map-padding\": \"48px\",\r\n};\r\n\r\n// Helper function to create theme\r\nfunction createTheme(\r\n  name: string,\r\n  type: \"light\" | \"dark\",\r\n  colors: {\r\n    mainColor: string;\r\n    mainBgcolor: string;\r\n    color: string;\r\n    bgcolor: string;\r\n    selected: string;\r\n    accentColor: string;\r\n    rootColor: string;\r\n    rootBgcolor: string;\r\n    rootBorderColor: string;\r\n    panelColor: string;\r\n    panelBgcolor: string;\r\n    panelBorderColor: string;\r\n  },\r\n  palette: string[],\r\n): MindElixirTheme {\r\n  return {\r\n    name,\r\n    type,\r\n    palette,\r\n    cssVar: {\r\n      ...commonSpacing,\r\n      \"--main-color\": colors.mainColor,\r\n      \"--main-bgcolor\": colors.mainBgcolor,\r\n      \"--main-bgcolor-transparent\": `${colors.mainBgcolor.replace(\")\", \" / 95%)\")}`,\r\n      \"--color\": colors.color,\r\n      \"--bgcolor\": colors.bgcolor,\r\n      \"--selected\": colors.selected,\r\n      \"--accent-color\": colors.accentColor,\r\n      \"--root-color\": colors.rootColor,\r\n      \"--root-bgcolor\": colors.rootBgcolor,\r\n      \"--root-border-color\": colors.rootBorderColor,\r\n      \"--panel-color\": colors.panelColor,\r\n      \"--panel-bgcolor\": colors.panelBgcolor,\r\n      \"--panel-border-color\": colors.panelBorderColor,\r\n    },\r\n  };\r\n}\r\n\r\n// Base color configurations\r\nconst lightColors = {\r\n  mainColor: \"oklch(0.145 0 0)\", // foreground\r\n  mainBgcolor: \"oklch(1 0 0)\", // background (white)\r\n  color: \"oklch(0.145 0 0)\", // foreground\r\n  bgcolor: \"oklch(1 0 0)\", // card background\r\n  selected: \"oklch(0.205 0 0)\", // primary\r\n  rootColor: \"oklch(0.985 0 0)\", // primary-foreground\r\n  rootBgcolor: \"oklch(0.205 0 0)\", // primary\r\n  rootBorderColor: \"oklch(0.205 0 0)\", // primary\r\n  panelColor: \"oklch(0.145 0 0)\", // foreground\r\n  panelBgcolor: \"oklch(1 0 0)\", // popover\r\n  panelBorderColor: \"oklch(0.922 0 0)\", // border\r\n};\r\n\r\nconst darkColors = {\r\n  mainColor: \"oklch(0.985 0 0)\", // foreground\r\n  mainBgcolor: \"oklch(0.145 0 0)\", // background (dark)\r\n  color: \"oklch(0.985 0 0)\", // foreground\r\n  bgcolor: \"oklch(0.205 0 0)\", // card background\r\n  selected: \"oklch(0.922 0 0)\", // primary\r\n  rootColor: \"oklch(0.205 0 0)\", // primary-foreground\r\n  rootBgcolor: \"oklch(0.922 0 0)\", // primary\r\n  rootBorderColor: \"oklch(0.922 0 0)\", // primary\r\n  panelColor: \"oklch(0.985 0 0)\", // foreground\r\n  panelBgcolor: \"oklch(0.205 0 0)\", // popover\r\n  panelBorderColor: \"oklch(1 0 0 / 10%)\", // border\r\n};\r\n\r\n// Shadcn-styled light theme\r\nconst lightTheme: MindElixirTheme = createTheme(\r\n  \"shadcn-light\",\r\n  \"light\",\r\n  {\r\n    ...lightColors,\r\n    accentColor: \"oklch(0.646 0.222 41.116)\", // chart-1 (vibrant)\r\n  },\r\n  [\r\n    \"oklch(0.646 0.222 41.116)\", // chart-1: vibrant orange\r\n    \"oklch(0.6 0.118 184.704)\", // chart-2: teal\r\n    \"oklch(0.398 0.07 227.392)\", // chart-3: blue\r\n    \"oklch(0.828 0.189 84.429)\", // chart-4: yellow-green\r\n    \"oklch(0.769 0.188 70.08)\", // chart-5: warm yellow\r\n    \"oklch(0.488 0.243 264.376)\", // purple\r\n    \"oklch(0.696 0.17 162.48)\", // mint\r\n  ],\r\n);\r\n\r\n// Shadcn-styled dark theme\r\nconst darkTheme: MindElixirTheme = createTheme(\r\n  \"shadcn-dark\",\r\n  \"dark\",\r\n  {\r\n    ...darkColors,\r\n    accentColor: \"oklch(0.488 0.243 264.376)\", // chart-1 (purple)\r\n  },\r\n  [\r\n    \"oklch(0.488 0.243 264.376)\", // chart-1: purple\r\n    \"oklch(0.696 0.17 162.48)\", // chart-2: mint\r\n    \"oklch(0.769 0.188 70.08)\", // chart-3: warm yellow\r\n    \"oklch(0.627 0.265 303.9)\", // chart-4: pink\r\n    \"oklch(0.645 0.246 16.439)\", // chart-5: coral\r\n    \"oklch(0.646 0.222 41.116)\", // orange\r\n    \"oklch(0.6 0.118 184.704)\", // teal\r\n  ],\r\n);\r\n\r\n// Monochrome variants - reuse base colors, only change accentColor and palette\r\nconst lightThemeMonochrome: MindElixirTheme = createTheme(\r\n  \"shadcn-light-mono\",\r\n  \"light\",\r\n  {\r\n    ...lightColors,\r\n    accentColor: \"oklch(0.205 0 0)\", // primary\r\n  },\r\n  [\"oklch(0.205 0 0)\"], // Single primary color\r\n);\r\n\r\nconst darkThemeMonochrome: MindElixirTheme = createTheme(\r\n  \"shadcn-dark-mono\",\r\n  \"dark\",\r\n  {\r\n    ...darkColors,\r\n    accentColor: \"oklch(0.922 0 0)\", // primary\r\n  },\r\n  [\"oklch(0.922 0 0)\"], // Single primary color\r\n);\r\n\r\n// Helper function to get the appropriate theme\r\nfunction getTheme(isDark: boolean, isMonochrome: boolean): MindElixirTheme {\r\n  if (isDark) {\r\n    return isMonochrome ? darkThemeMonochrome : darkTheme;\r\n  }\r\n  return isMonochrome ? lightThemeMonochrome : lightTheme;\r\n}\r\n\r\nconst SIDE = 2;\r\nexport const MindMap = forwardRef<MindMapRef, MindMapProps>(function MindMap(\r\n  {\r\n    children,\r\n    data,\r\n    className,\r\n    direction = SIDE,\r\n    contextMenu = true,\r\n    keypress = true,\r\n    locale = \"en\",\r\n    overflowHidden = false,\r\n    theme: themeProp,\r\n    monochrome = false,\r\n    fit = true,\r\n    readonly = false,\r\n    compact = false,\r\n    markdown,\r\n    imageProxy,\r\n    onChange,\r\n    onOperation,\r\n    onSelectNodes,\r\n    loader,\r\n  }: MindMapProps,\r\n  ref,\r\n) {\r\n  const containerRef = useRef<HTMLDivElement>(null);\r\n  const mindRef = useRef<MindElixirInstance | null>(null);\r\n  const [isLoaded, setIsLoaded] = useState(false);\r\n  const [mindInstance, setMindInstance] = useState<MindElixirInstance | null>(\r\n    null,\r\n  );\r\n  const resolvedTheme = useResolvedTheme(themeProp);\r\n  const id = useId();\r\n\r\n  // Expose mind instance to parent component via ref\r\n  useImperativeHandle(\r\n    ref,\r\n    () => ({\r\n      instance: mindRef.current,\r\n    }),\r\n    [],\r\n  );\r\n\r\n  // Store resolvedTheme in a ref for use in effects without triggering re-runs\r\n  const resolvedThemeRef = useRef(resolvedTheme);\r\n  useEffect(() => {\r\n    resolvedThemeRef.current = resolvedTheme;\r\n  }, [resolvedTheme]);\r\n\r\n  // Store callbacks in refs to avoid re-initialization when they change\r\n  const onChangeRef = useRef(onChange);\r\n  const onOperationRef = useRef(onOperation);\r\n  const onSelectNodesRef = useRef(onSelectNodes);\r\n  const markdownRef = useRef(markdown);\r\n  const imageProxyRef = useRef(imageProxy);\r\n\r\n  useEffect(() => {\r\n    onChangeRef.current = onChange;\r\n    onOperationRef.current = onOperation;\r\n    onSelectNodesRef.current = onSelectNodes;\r\n    markdownRef.current = markdown;\r\n    imageProxyRef.current = imageProxy;\r\n  }, [onChange, onOperation, onSelectNodes, markdown, imageProxy]);\r\n\r\n  // Track internal changes to avoid refresh loops\r\n  const isInternalChangeRef = useRef(false);\r\n\r\n  // Store initial data in ref - only used for initialization, not reactive\r\n  const initialDataRef = useRef(data);\r\n\r\n  // Initialize MindElixir (client-side only)\r\n  useEffect(() => {\r\n    if (!containerRef.current || mindRef.current) return;\r\n\r\n    let isSubscribed = true;\r\n\r\n    // Prioritize theme from data, then fall back to component props\r\n    const initialData =\r\n      toData(initialDataRef.current) || MindElixir.new(\"Mind Map\");\r\n    const themeToUse =\r\n      initialData.theme ||\r\n      getTheme(resolvedThemeRef.current === \"dark\", monochrome);\r\n\r\n    const options: Options = {\r\n      el: containerRef.current,\r\n      direction,\r\n      contextMenu,\r\n      toolBar: false,\r\n      keypress,\r\n      locale,\r\n      overflowHidden,\r\n      editable: !readonly,\r\n      compact,\r\n      alignment: \"nodes\",\r\n      theme: themeToUse,\r\n      markdown: markdownRef.current\r\n        ? (text, obj) => markdownRef.current?.(text, obj) ?? text\r\n        : undefined,\r\n      imageProxy: imageProxyRef.current\r\n        ? (url) => imageProxyRef.current?.(url) ?? url\r\n        : undefined,\r\n    };\r\n\r\n    try {\r\n      const mind = new MindElixir(options);\r\n      mind.init(initialData);\r\n\r\n      if (isSubscribed) {\r\n        mindRef.current = mind;\r\n        setMindInstance(mind);\r\n        setIsLoaded(true);\r\n\r\n        // Auto-fit if enabled\r\n        if (fit) {\r\n          mind.scaleFit();\r\n        }\r\n\r\n        // Event listeners (using refs to avoid re-initialization)\r\n        mind.bus.addListener(\"operation\", (operation) => {\r\n          // Call onOperation if provided\r\n          if (onOperationRef.current) {\r\n            onOperationRef.current(operation);\r\n          }\r\n          // Call onChange if provided\r\n          if (onChangeRef.current) {\r\n            const updatedData = mind.getData();\r\n            // Mark this as an internal change to prevent refresh loop\r\n            isInternalChangeRef.current = true;\r\n            onChangeRef.current(updatedData, operation);\r\n          }\r\n        });\r\n\r\n        if (onSelectNodesRef.current) {\r\n          mind.bus.addListener(\"selectNodes\", (nodeObj) => {\r\n            onSelectNodesRef.current?.(nodeObj);\r\n          });\r\n        }\r\n      }\r\n    } catch (error) {\r\n      console.error(\"Failed to initialize MindElixir:\", error);\r\n    }\r\n\r\n    return () => {\r\n      isSubscribed = false;\r\n      // Note: We intentionally don't clean up the mind instance here\r\n      // to avoid DOM manipulation conflicts with React\r\n      // The instance will be garbage collected when the component unmounts\r\n      mindRef.current = null;\r\n    };\r\n  }, [\r\n    direction,\r\n    contextMenu,\r\n    keypress,\r\n    locale,\r\n    overflowHidden,\r\n    monochrome,\r\n    readonly,\r\n    fit,\r\n  ]);\r\n\r\n  // Update data when it changes\r\n  useEffect(() => {\r\n    if (mindRef.current && data && isLoaded) {\r\n      // Skip refresh if this change came from onChange (internal change)\r\n      if (isInternalChangeRef.current) {\r\n        isInternalChangeRef.current = false;\r\n        return;\r\n      }\r\n      const next = toData(data);\r\n      if (next) {\r\n        mindRef.current.refresh(next);\r\n      }\r\n    }\r\n  }, [data, isLoaded]);\r\n\r\n  // Update theme when resolvedTheme or monochrome changes\r\n  // BUT only if the data itself doesn't have a theme (data.theme has highest priority)\r\n  // Plaintext strings never carry a theme, so only object data is checked.\r\n  const dataTheme = typeof data === \"object\" ? data?.theme : undefined;\r\n  useEffect(() => {\r\n    if (!mindRef.current || !isLoaded) return;\r\n\r\n    // Check if the provided data prop has its own theme\r\n    if (dataTheme) {\r\n      // Data has its own theme, don't override it with prop changes\r\n      return;\r\n    }\r\n\r\n    // No theme in data, apply theme from props\r\n    const newTheme = getTheme(resolvedTheme === \"dark\", monochrome);\r\n    mindRef.current.changeTheme(newTheme);\r\n  }, [resolvedTheme, monochrome, isLoaded, dataTheme]);\r\n\r\n  // Update compact mode when prop changes (after init)\r\n  const compactInitializedRef = useRef(false);\r\n  useEffect(() => {\r\n    if (!mindRef.current || !isLoaded) return;\r\n    // Skip first run — compact is already applied via Options at init\r\n    if (!compactInitializedRef.current) {\r\n      compactInitializedRef.current = true;\r\n      return;\r\n    }\r\n    mindRef.current.changeCompact(compact);\r\n  }, [compact, isLoaded]);\r\n\r\n  // Keep markdown / imageProxy in sync without re-initializing the map\r\n  const parsersInitializedRef = useRef(false);\r\n  useEffect(() => {\r\n    if (!mindRef.current || !isLoaded) return;\r\n    mindRef.current.markdown = markdown\r\n      ? (text, obj) => markdown(text, obj)\r\n      : undefined;\r\n    mindRef.current.imageProxy = imageProxy\r\n      ? (url) => imageProxy(url)\r\n      : undefined;\r\n    // Skip first run — parsers are already applied via Options at init\r\n    if (!parsersInitializedRef.current) {\r\n      parsersInitializedRef.current = true;\r\n      return;\r\n    }\r\n    // Re-render nodes so updated parsers take effect\r\n    mindRef.current.refresh();\r\n  }, [markdown, imageProxy, isLoaded]);\r\n\r\n  return (\r\n    <MindMapContext.Provider value={{ mind: mindInstance, isLoaded }}>\r\n      <div className={cn(\"relative w-full h-full bg-background rounded-lg overflow-hidden\", className)}>\r\n        <div\r\n          key={id}\r\n          ref={containerRef}\r\n          id={`mindmap-${id}`}\r\n          className=\"w-full h-full\"\r\n        />\r\n        {!isLoaded ? loader || <DefaultLoader /> : null}\r\n        {children}\r\n      </div>\r\n    </MindMapContext.Provider>\r\n  );\r\n});\r\n\r\n// MindMap Controls\r\ninterface MindMapControlsProps {\r\n  position?: \"top-left\" | \"top-right\" | \"bottom-left\" | \"bottom-right\";\r\n  showZoom?: boolean;\r\n  showFit?: boolean;\r\n  showExport?: boolean;\r\n  className?: string;\r\n  onExport?: (file: Blob, filename: string) => void;\r\n}\r\n\r\nexport function MindMapControls({\r\n  position = \"top-right\",\r\n  showZoom = true,\r\n  showFit = true,\r\n  showExport = true,\r\n  className,\r\n  onExport,\r\n}: MindMapControlsProps) {\r\n  const { mind, isLoaded } = useMindMap();\r\n  const [mounted, setMounted] = useState(false);\r\n  const [isFullscreen, setIsFullscreen] = useState(false);\r\n\r\n  useEffect(() => {\r\n    const timer = setTimeout(() => setMounted(true), 0);\r\n    return () => clearTimeout(timer);\r\n  }, []);\r\n\r\n  const handleZoomIn = () => {\r\n    if (mind) {\r\n      const currentScale = mind.scaleVal || 1;\r\n      mind.scale(currentScale + 0.2);\r\n    }\r\n  };\r\n\r\n  const handleZoomOut = () => {\r\n    if (mind) {\r\n      const currentScale = mind.scaleVal || 1;\r\n      mind.scale(Math.max(0.2, currentScale - 0.2));\r\n    }\r\n  };\r\n\r\n  const handleFit = () => {\r\n    if (mind) {\r\n      mind.scaleFit();\r\n    }\r\n  };\r\n\r\n  const handleExport = async () => {\r\n    if (mind) {\r\n      try {\r\n        // Export as image using the @mind-elixir/export-mindmap plugin\r\n        const blob = await exportImageBlob(mind, \"jpeg\", {\r\n          watermarkEnabled: false,\r\n        });\r\n        // Use root node's topic as filename\r\n        const rootTopic = mind.nodeData.topic || \"mindmap\";\r\n        const filename = `${rootTopic}.jpg`;\r\n\r\n        // Provide the blob to the callback\r\n        if (onExport) {\r\n          onExport(blob, filename);\r\n        }\r\n\r\n        // Download the file\r\n        const url = URL.createObjectURL(blob);\r\n        const link = document.createElement(\"a\");\r\n        link.download = filename;\r\n        link.href = url;\r\n        link.click();\r\n        URL.revokeObjectURL(url);\r\n      } catch (error) {\r\n        console.error(\"Failed to export mind map:\", error);\r\n      }\r\n    }\r\n  };\r\n\r\n  const handleFullscreen = () => {\r\n    const container = mind?.container?.parentElement;\r\n    if (!container) return;\r\n\r\n    if (!document.fullscreenElement) {\r\n      container\r\n        .requestFullscreen()\r\n        .then(() => {\r\n          setIsFullscreen(true);\r\n        })\r\n        .catch((err) => {\r\n          console.error(\"Failed to enter fullscreen:\", err);\r\n        });\r\n    } else {\r\n      document.exitFullscreen().then(() => {\r\n        setIsFullscreen(false);\r\n      });\r\n    }\r\n  };\r\n\r\n  // Listen for fullscreen changes\r\n  useEffect(() => {\r\n    const handleFullscreenChange = () => {\r\n      const isNowFullscreen = !!document.fullscreenElement;\r\n      setIsFullscreen(isNowFullscreen);\r\n\r\n      // When exiting fullscreen, call scaleFit to ensure content is visible\r\n      if (!isNowFullscreen && mind) {\r\n        mind.scaleFit();\r\n      }\r\n    };\r\n\r\n    document.addEventListener(\"fullscreenchange\", handleFullscreenChange);\r\n    return () => {\r\n      document.removeEventListener(\"fullscreenchange\", handleFullscreenChange);\r\n    };\r\n  }, [mind]);\r\n\r\n  if (!mounted || !isLoaded) return null;\r\n\r\n  const positionClasses = {\r\n    \"top-left\": \"top-3 left-3\",\r\n    \"top-right\": \"top-3 right-3\",\r\n    \"bottom-left\": \"bottom-3 left-3\",\r\n    \"bottom-right\": \"bottom-3 right-3\",\r\n  };\r\n\r\n  return (\r\n    <div\r\n      className={cn(\r\n        \"absolute z-10 flex flex-col gap-1\",\r\n        positionClasses[position],\r\n        className,\r\n      )}\r\n    >\r\n      {showZoom && (\r\n        <>\r\n          <button\r\n            onClick={handleZoomIn}\r\n            className=\"size-8 rounded-md bg-background/95 backdrop-blur-md border border-border/50 shadow-lg flex items-center justify-center hover:bg-accent transition-colors\"\r\n            aria-label=\"Zoom in\"\r\n          >\r\n            <Plus className=\"size-4\" />\r\n          </button>\r\n          <button\r\n            onClick={handleZoomOut}\r\n            className=\"size-8 rounded-md bg-background/95 backdrop-blur-md border border-border/50 shadow-lg flex items-center justify-center hover:bg-accent transition-colors\"\r\n            aria-label=\"Zoom out\"\r\n          >\r\n            <Minus className=\"size-4\" />\r\n          </button>\r\n        </>\r\n      )}\r\n      {showFit && (\r\n        <button\r\n          onClick={handleFit}\r\n          className=\"size-8 rounded-md bg-background/95 backdrop-blur-md border border-border/50 shadow-lg flex items-center justify-center hover:bg-accent transition-colors\"\r\n          aria-label=\"Fit to screen\"\r\n        >\r\n          <ScanSearch className=\"size-4\" />\r\n        </button>\r\n      )}\r\n      <button\r\n        onClick={handleFullscreen}\r\n        className=\"size-8 rounded-md bg-background/95 backdrop-blur-md border border-border/50 shadow-lg flex items-center justify-center hover:bg-accent transition-colors\"\r\n        aria-label={isFullscreen ? \"Exit fullscreen\" : \"Enter fullscreen\"}\r\n      >\r\n        <Maximize className=\"size-4\" />\r\n      </button>\r\n      {showExport && (\r\n        <button\r\n          onClick={handleExport}\r\n          className=\"size-8 rounded-md bg-background/95 backdrop-blur-md border border-border/50 shadow-lg flex items-center justify-center hover:bg-accent transition-colors\"\r\n          aria-label=\"Download as image\"\r\n        >\r\n          <Download className=\"size-4\" />\r\n        </button>\r\n      )}\r\n    </div>\r\n  );\r\n}\r\n\r\n// Export components\r\n",
      "type": "registry:ui",
      "target": "components/ui/mindmap.tsx"
    }
  ],
  "css": {},
  "meta": {
    "components": [
      {
        "title": "MindMap",
        "href": "/llm/mindmap",
        "description": "Root Mind Elixir map component for presentation-first diagrams."
      },
      {
        "title": "MindMapControls",
        "href": "/docs/api-reference",
        "description": "Zoom, fit-to-view, and export controls."
      },
      {
        "title": "useMindMap",
        "href": "/docs/api-reference",
        "description": "Access the Mind Elixir instance and load state from child components."
      }
    ]
  },
  "type": "registry:ui"
}