{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "code-drawing-node",
  "title": "Code Drawing Node",
  "description": "Create diagrams from code using PlantUML, Graphviz, Flowchart, or Mermaid.",
  "dependencies": [
    "@platejs/code-drawing"
  ],
  "registryDependencies": [
    "popover",
    "button",
    "select",
    "https://platejs.org/r/use-mobile.json"
  ],
  "files": [
    {
      "path": "src/registry/ui/code-drawing-node.tsx",
      "content": "'use client';\n\nimport * as React from 'react';\n\nimport type {\n  CodeDrawingType,\n  TCodeDrawingElement,\n  ViewMode,\n} from '@platejs/code-drawing';\nimport {\n  VIEW_MODE,\n  DEFAULT_MIN_HEIGHT,\n  CODE_DRAWING_TYPE_ARRAY,\n  VIEW_MODE_ARRAY,\n  renderCodeDrawing,\n  RENDER_DEBOUNCE_DELAY,\n  downloadImage,\n  DOWNLOAD_FILENAME,\n} from '@platejs/code-drawing';\nimport type { PlateElementProps } from 'platejs/react';\nimport {\n  PlateElement,\n  useEditorRef,\n  useEditorSelector,\n  useElement,\n  useFocusedLast,\n  useReadOnly,\n  useSelected,\n} from 'platejs/react';\nimport debounce from 'lodash/debounce.js';\nimport { Trash2, DownloadIcon } from 'lucide-react';\n\nimport { Button } from '@/components/ui/button';\nimport { useIsMobile } from '@/hooks/use-mobile';\nimport {\n  Popover,\n  PopoverAnchor,\n  PopoverContent,\n} from '@/components/ui/popover';\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from '@/components/ui/select';\n\nfunction createDebouncedCodeDrawingRenderer(\n  setImage: React.Dispatch<React.SetStateAction<string>>,\n  setLoading: React.Dispatch<React.SetStateAction<boolean>>,\n  setError: React.Dispatch<React.SetStateAction<string | null>>\n) {\n  let lastRequestId = 0;\n\n  return debounce(\n    async (code: string | undefined, drawingType: string | undefined) => {\n      lastRequestId += 1;\n      const requestId = lastRequestId;\n\n      if (!code?.trim() || !drawingType) {\n        setImage('');\n        setLoading(false);\n        setError(null);\n        return;\n      }\n\n      setLoading(true);\n      setError(null);\n\n      try {\n        const imageData = await renderCodeDrawing(\n          drawingType as CodeDrawingType,\n          code\n        );\n\n        if (lastRequestId === requestId) {\n          setImage(imageData);\n          setError(null);\n        }\n      } catch (err) {\n        if (lastRequestId === requestId) {\n          setError(err instanceof Error ? err.message : 'Rendering failed');\n          setImage('');\n        }\n      } finally {\n        if (lastRequestId === requestId) {\n          setLoading(false);\n        }\n      }\n    },\n    RENDER_DEBOUNCE_DELAY\n  );\n}\n\nfunction useCodeDrawingElement({ element }: { element: TCodeDrawingElement }) {\n  const editor = useEditorRef();\n  const readOnly = useReadOnly();\n  const [loading, setLoading] = React.useState(false);\n  const [error, setError] = React.useState<string | null>(null);\n  const [image, setImage] = React.useState<string>('');\n\n  const debouncedRender = React.useMemo(\n    () => createDebouncedCodeDrawingRenderer(setImage, setLoading, setError),\n    []\n  );\n\n  React.useEffect(() => {\n    debouncedRender(element.data?.code, element.data?.drawingType);\n\n    return () => {\n      debouncedRender.cancel();\n    };\n  }, [element.data?.code, element.data?.drawingType, debouncedRender]);\n\n  const removeNode = () => {\n    if (readOnly) return;\n\n    const path = editor.api.findPath(element);\n    if (path) {\n      editor.tf.removeNodes({ at: path });\n    }\n  };\n\n  return {\n    loading,\n    error,\n    image,\n    removeNode,\n  };\n}\n\nexport function CodeDrawingElement(\n  props: PlateElementProps<TCodeDrawingElement>\n) {\n  const { children } = props;\n  const isMobile = useIsMobile();\n  const editor = useEditorRef();\n  const readOnly = useReadOnly();\n  const selected = useSelected();\n  const isFocusedLast = useFocusedLast();\n  const element = useElement<TCodeDrawingElement>();\n  const { removeNode, image, loading } = useCodeDrawingElement({ element });\n\n  const handleDownload = React.useCallback(() => {\n    if (!image) return;\n    downloadImage(image, DOWNLOAD_FILENAME);\n  }, [image]);\n\n  const handleCodeChange = React.useCallback(\n    (code: string) => {\n      const path = editor.api.findPath(element);\n      if (path) {\n        editor.tf.setNodes(\n          {\n            data: {\n              ...element.data,\n              code,\n            },\n          },\n          { at: path }\n        );\n      }\n    },\n    [editor, element]\n  );\n\n  const handleDrawingTypeChange = React.useCallback(\n    (drawingType: CodeDrawingType) => {\n      const path = editor.api.findPath(element);\n      if (path) {\n        editor.tf.setNodes(\n          {\n            data: {\n              ...element.data,\n              drawingType,\n            },\n          },\n          { at: path }\n        );\n      }\n    },\n    [editor, element]\n  );\n\n  const handleDrawingModeChange = React.useCallback(\n    (drawingMode: ViewMode) => {\n      const path = editor.api.findPath(element);\n      if (path) {\n        editor.tf.setNodes(\n          {\n            data: {\n              ...element.data,\n              drawingMode,\n            },\n          },\n          { at: path }\n        );\n      }\n    },\n    [editor, element]\n  );\n\n  const code = element.data?.code ?? '';\n  const drawingType = element.data?.drawingType ?? 'Mermaid';\n  const drawingMode = element.data?.drawingMode ?? 'Both';\n\n  const selectionCollapsed = useEditorSelector(\n    (editor) => !editor.api.isExpanded(),\n    []\n  );\n\n  const open = isFocusedLast && !readOnly && selected && selectionCollapsed;\n\n  const content = (\n    <PlateElement {...props}>\n      <div contentEditable={false}>\n        <div>\n          <CodeDrawingPreview\n            code={code}\n            drawingType={drawingType}\n            drawingMode={drawingMode}\n            image={image}\n            loading={loading}\n            onCodeChange={handleCodeChange}\n            onDrawingTypeChange={handleDrawingTypeChange}\n            onDrawingModeChange={handleDrawingModeChange}\n            readOnly={readOnly}\n            isMobile={isMobile}\n          />\n        </div>\n      </div>\n      {children}\n    </PlateElement>\n  );\n\n  if (readOnly) {\n    return content;\n  }\n\n  return (\n    <Popover open={open} modal={false}>\n      <PopoverAnchor asChild>{content}</PopoverAnchor>\n      <PopoverContent\n        className=\"w-auto p-1\"\n        contentEditable={false}\n        onOpenAutoFocus={(e) => e.preventDefault()}\n      >\n        <div className=\"flex items-center gap-1\">\n          {image && (\n            <Button\n              size=\"icon\"\n              variant=\"ghost\"\n              className=\"size-8\"\n              onClick={handleDownload}\n              title=\"Export\"\n            >\n              <DownloadIcon className=\"size-4\" />\n            </Button>\n          )}\n          <Button\n            size=\"icon\"\n            variant=\"ghost\"\n            className=\"size-8\"\n            onClick={removeNode}\n            title=\"Delete\"\n          >\n            <Trash2 className=\"size-4\" />\n          </Button>\n        </div>\n      </PopoverContent>\n    </Popover>\n  );\n}\n\nfunction CodeDrawingPreview({\n  code,\n  drawingType,\n  drawingMode,\n  image,\n  loading,\n  onCodeChange,\n  onDrawingTypeChange,\n  onDrawingModeChange,\n  readOnly = false,\n  isMobile = false,\n}: {\n  code: string;\n  drawingType: CodeDrawingType;\n  drawingMode: ViewMode;\n  image: string;\n  loading: boolean;\n  onCodeChange: (code: string) => void;\n  onDrawingTypeChange: (type: CodeDrawingType) => void;\n  onDrawingModeChange: (mode: ViewMode) => void;\n  readOnly?: boolean;\n  isMobile?: boolean;\n}) {\n  const viewMode = drawingMode;\n  const showCode = viewMode === VIEW_MODE.Both || viewMode === VIEW_MODE.Code;\n  const showBorder = viewMode === VIEW_MODE.Both;\n\n  const handleCodeChange = React.useCallback(\n    (e: React.ChangeEvent<HTMLTextAreaElement>) => {\n      onCodeChange(e.target.value);\n    },\n    [onCodeChange]\n  );\n\n  const toolbar = readOnly ? null : (\n    <CodeDrawingToolbar\n      drawingType={drawingType}\n      viewMode={viewMode}\n      readOnly={readOnly}\n      isMobile={isMobile}\n      onDrawingTypeChange={onDrawingTypeChange}\n      onDrawingModeChange={onDrawingModeChange}\n    />\n  );\n\n  return (\n    <div\n      className={`flex ${isMobile ? 'flex-col-reverse' : 'flex-col'} group my-4 w-full items-stretch border bg-muted/50 md:flex-row`}\n      style={{\n        minHeight: `${DEFAULT_MIN_HEIGHT}px`,\n      }}\n    >\n      {showCode && (\n        <CodeDrawingTextarea\n          code={code}\n          viewMode={viewMode}\n          readOnly={readOnly}\n          isMobile={isMobile}\n          showBorder={showBorder}\n          onCodeChange={handleCodeChange}\n          toolbar={viewMode === VIEW_MODE.Code ? toolbar : null}\n        />\n      )}\n\n      {viewMode !== VIEW_MODE.Code && (\n        <CodeDrawingPreviewArea\n          image={image}\n          loading={loading}\n          code={code}\n          viewMode={viewMode}\n          readOnly={readOnly}\n          isMobile={isMobile}\n          showBorder={showBorder}\n          toolbar={toolbar}\n        />\n      )}\n    </div>\n  );\n}\n\nfunction CodeDrawingToolbar({\n  drawingType,\n  viewMode,\n  readOnly = false,\n  isMobile = false,\n  onDrawingTypeChange,\n  onDrawingModeChange,\n}: {\n  drawingType: CodeDrawingType;\n  viewMode: ViewMode;\n  readOnly?: boolean;\n  isMobile?: boolean;\n  onDrawingTypeChange: (type: CodeDrawingType) => void;\n  onDrawingModeChange: (mode: ViewMode) => void;\n}) {\n  const [toolbarVisible, setToolbarVisible] = React.useState(false);\n  const [languageSelectOpen, setLanguageSelectOpen] = React.useState(false);\n  const [viewModeSelectOpen, setViewModeSelectOpen] = React.useState(false);\n\n  const opacityClass =\n    isMobile || toolbarVisible || languageSelectOpen || viewModeSelectOpen\n      ? 'opacity-100'\n      : 'opacity-0 group-hover:opacity-100';\n\n  const positionClass = isMobile\n    ? 'flex items-center gap-2'\n    : 'absolute right-2 z-10 flex items-center gap-2';\n\n  return (\n    <div\n      role=\"toolbar\"\n      className={`${positionClass} transition-opacity ${opacityClass}`}\n      onMouseEnter={() => setToolbarVisible(true)}\n      onMouseLeave={() => {\n        if (!languageSelectOpen && !viewModeSelectOpen) {\n          setToolbarVisible(false);\n        }\n      }}\n    >\n      {!readOnly && (\n        <Select\n          value={drawingType}\n          onValueChange={onDrawingTypeChange}\n          open={languageSelectOpen}\n          onOpenChange={setLanguageSelectOpen}\n        >\n          <SelectTrigger\n            className={`h-8 w-[120px] border-0 bg-muted/50 text-xs shadow-none ${\n              isMobile ? '' : 'transition-colors hover:bg-zinc-200'\n            }`}\n          >\n            <SelectValue />\n          </SelectTrigger>\n          <SelectContent className=\"z-[100]\">\n            {CODE_DRAWING_TYPE_ARRAY.map((item) => (\n              <SelectItem key={item.value} value={item.value}>\n                {item.label}\n              </SelectItem>\n            ))}\n          </SelectContent>\n        </Select>\n      )}\n\n      {!readOnly && (\n        <Select\n          value={viewMode}\n          onValueChange={onDrawingModeChange}\n          open={viewModeSelectOpen}\n          onOpenChange={setViewModeSelectOpen}\n        >\n          <SelectTrigger\n            className={`h-8 w-[80px] border-0 bg-muted/50 text-xs shadow-none ${\n              isMobile ? '' : 'transition-colors hover:bg-zinc-200'\n            }`}\n          >\n            <SelectValue />\n          </SelectTrigger>\n          <SelectContent className=\"z-[100]\">\n            {VIEW_MODE_ARRAY.map((item) => (\n              <SelectItem key={item.value} value={item.value}>\n                {item.label}\n              </SelectItem>\n            ))}\n          </SelectContent>\n        </Select>\n      )}\n    </div>\n  );\n}\n\nfunction CodeDrawingTextarea({\n  code,\n  viewMode,\n  readOnly = false,\n  isMobile = false,\n  showBorder = false,\n  onCodeChange,\n  toolbar,\n}: {\n  code: string;\n  viewMode: ViewMode;\n  readOnly?: boolean;\n  isMobile?: boolean;\n  showBorder?: boolean;\n  onCodeChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;\n  toolbar?: React.ReactNode;\n}) {\n  const textareaRef = React.useRef<HTMLTextAreaElement>(null);\n  const isCodeOnlyMode = viewMode === VIEW_MODE.Code;\n\n  const [internalCode, setInternalCode] = React.useState(code);\n  const lastExternalCodeRef = React.useRef(code);\n\n  React.useEffect(() => {\n    if (code !== lastExternalCodeRef.current) {\n      lastExternalCodeRef.current = code;\n      setInternalCode(code);\n    }\n  }, [code]);\n\n  const handleChange = React.useCallback(\n    (e: React.ChangeEvent<HTMLTextAreaElement>) => {\n      const newValue = e.target.value;\n      setInternalCode(newValue);\n      onCodeChange(e);\n    },\n    [onCodeChange]\n  );\n\n  return (\n    <div\n      className={`${\n        isCodeOnlyMode ? 'w-full' : 'min-w-0 flex-1'\n      } flex flex-col ${isCodeOnlyMode && !isMobile ? 'relative' : ''} ${\n        showBorder && !isMobile ? 'border-r' : ''\n      }`}\n    >\n      {toolbar && isCodeOnlyMode && (\n        <div\n          className={\n            isMobile\n              ? 'mt-2 mb-2 flex justify-end px-2'\n              : 'absolute right-2 z-10 mt-2'\n          }\n        >\n          {toolbar}\n        </div>\n      )}\n\n      <div className=\"relative flex-1 rounded-md\">\n        <pre\n          className={\n            'm-0 overflow-x-auto p-8 pr-4 font-mono text-sm leading-[normal] [tab-size:2] print:break-inside-avoid'\n          }\n          style={{ minHeight: `${DEFAULT_MIN_HEIGHT}px`, height: '100%' }}\n        >\n          <code className=\"block h-full w-full\">\n            <textarea\n              ref={textareaRef}\n              value={internalCode}\n              onChange={handleChange}\n              readOnly={readOnly}\n              className=\"m-0 h-full w-full resize-none overflow-auto border-0 bg-transparent p-0 font-mono text-sm outline-none\"\n              style={{ minHeight: `${DEFAULT_MIN_HEIGHT}px` }}\n              placeholder=\"Enter your code here...\"\n              spellCheck={false}\n            />\n          </code>\n        </pre>\n      </div>\n    </div>\n  );\n}\n\nfunction CodeDrawingPreviewArea({\n  image,\n  loading,\n  code,\n  viewMode,\n  readOnly: _readOnly = false,\n  isMobile = false,\n  showBorder = false,\n  toolbar,\n}: {\n  image: string;\n  loading: boolean;\n  code: string;\n  viewMode: ViewMode;\n  readOnly?: boolean;\n  isMobile?: boolean;\n  showBorder?: boolean;\n  toolbar?: React.ReactNode;\n}) {\n  const showImage = viewMode === VIEW_MODE.Both || viewMode === VIEW_MODE.Image;\n\n  return (\n    <div\n      className={`flex min-w-0 flex-1 flex-col ${isMobile ? '' : 'relative'} ${\n        showBorder && isMobile ? 'border-b' : ''\n      }`}\n    >\n      {toolbar && (\n        <div\n          className={\n            isMobile\n              ? 'mt-2 mb-2 flex justify-end px-2'\n              : 'absolute right-2 z-10 mt-2'\n          }\n        >\n          {toolbar}\n        </div>\n      )}\n\n      {showImage ? (\n        <div\n          className={\n            'flex flex-1 items-center justify-center rounded-md bg-muted/30 p-4'\n          }\n        >\n          {loading && <div className=\"text-muted-foreground\">Loading...</div>}\n          {!loading && image && (\n            <img\n              src={image}\n              alt=\"Code drawing\"\n              className=\"max-h-full max-w-full object-contain\"\n            />\n          )}\n          {!loading && !image && (\n            <div className=\"text-muted-foreground\">\n              {code.trim() ? 'Rendering...' : 'Preview will appear here'}\n            </div>\n          )}\n        </div>\n      ) : (\n        <div className=\"pointer-events-none flex flex-1 items-center justify-center rounded-md border bg-muted/30 p-4 opacity-0\">\n          {/* Placeholder to maintain height */}\n        </div>\n      )}\n    </div>\n  );\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "src/registry/ui/code-drawing-node-static.tsx",
      "content": "import * as React from 'react';\n\nimport type { TCodeDrawingElement } from '@platejs/code-drawing';\nimport type { SlateElementProps } from 'platejs/static';\n\nimport { cn } from '@/lib/utils';\nimport { SlateElement } from 'platejs/static';\n\nexport function CodeDrawingElementStatic({\n  children,\n  ...props\n}: SlateElementProps<TCodeDrawingElement>) {\n  return (\n    <SlateElement className=\"my-4 flex w-full items-stretch\" {...props}>\n      <div className={cn('flex w-full flex-col md:flex-row')}>\n        <div className=\"relative h-full min-w-0 flex-1 rounded-md bg-muted/50 p-8 pr-4\">\n          <pre className=\"m-0 overflow-x-auto font-mono text-sm leading-[normal] [tab-size:2] print:break-inside-avoid\">\n            <code className=\"block w-full\">\n              {(props.element.data?.code as string) ||\n                'Enter your code here...'}\n            </code>\n          </pre>\n        </div>\n        <div className=\"relative flex min-w-0 flex-1 items-center justify-center rounded-md border bg-muted/30 p-4\">\n          <div className=\"text-muted-foreground\">\n            {props.element.data?.drawingType || 'Mermaid'}\n          </div>\n        </div>\n      </div>\n      {children}\n    </SlateElement>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "meta": {
    "docs": [
      {
        "route": "/docs/code-drawing"
      },
      {
        "route": "https://pro.platejs.org/docs/components/code-drawing-node"
      }
    ],
    "examples": [
      "code-drawing-demo"
    ]
  },
  "type": "registry:ui"
}