{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "markdown-streaming-demo",
  "dependencies": [],
  "registryDependencies": [
    "https://platejs.org/r/copilot-kit.json",
    "https://platejs.org/r/editor-kit.json",
    "https://platejs.org/r/markdown-joiner-transform.json"
  ],
  "files": [
    {
      "path": "src/registry/examples/markdown-streaming-demo.tsx",
      "content": "'use client';\nimport {\n  type HTMLAttributes,\n  useCallback,\n  useReducer,\n  useRef,\n  useState,\n} from 'react';\n\nimport { AIChatPlugin, streamInsertChunk } from '@platejs/ai/react';\nimport { deserializeMd } from '@platejs/markdown';\nimport {\n  ChevronFirstIcon,\n  ChevronLastIcon,\n  PauseIcon,\n  PlayIcon,\n  RotateCcwIcon,\n} from 'lucide-react';\nimport { getPluginType, KEYS } from 'platejs';\nimport { Plate, usePlateEditor, usePlateViewEditor } from 'platejs/react';\n\nimport { Button } from '@/components/ui/button';\nimport { cn } from '@/lib/utils';\nimport { EditorKit } from '@/registry/components/editor/editor-kit';\nimport { CopilotKit } from '@/registry/components/editor/plugins/copilot-kit';\nimport { MarkdownJoiner } from '@/registry/lib/markdown-joiner-transform';\nimport { Editor, EditorContainer, EditorView } from '@/registry/ui/editor';\n\nimport { BaseEditorKit } from '../components/editor/editor-base-kit';\n\nconst CAPITALIZE_REGEX = /([A-Z])/g;\nconst FIRST_CHAR_REGEX = /^./;\nconst TRAILING_NEWLINES_REGEX = /(\\n+)$/;\n\nconst testScenarios = {\n  // Basic markdown with complete elements\n  columns: [\n    'paragraph\\n\\n<column',\n    '_group',\n    '>\\n',\n    ' ',\n    ' <',\n    'column',\n    ' width',\n    '=\"',\n    '33',\n    '.',\n    '333',\n    '333',\n    '333',\n    '333',\n    '336',\n    '%\">\\n',\n    '   ',\n    ' ',\n    '1',\n    '\\n',\n    ' ',\n    ' </',\n    'column',\n    '>\\n',\n    ' ',\n    ' <',\n    'column',\n    ' width',\n    '=\"',\n    '33',\n    '.',\n    '333',\n    '333',\n    '333',\n    '333',\n    '336',\n    '%\">\\n',\n    '   ',\n    ' ',\n    '2',\n    '\\n',\n    ' ',\n    ' </',\n    'column',\n    '>\\n',\n    ' ',\n    ' <',\n    'column',\n    ' width',\n    '=\"',\n    '33',\n    '.',\n    '333',\n    '333',\n    '333',\n    '333',\n    '336',\n    '%\">\\n',\n    '   ',\n    ' ',\n    '3',\n    '\\n',\n    ' ',\n    ' </',\n    'column',\n    '>\\n',\n    '</',\n    'column',\n    '_group',\n    '>\\n\\nparagraph',\n  ],\n  links: [\n    '[Link ',\n    'to OpenA',\n    'I](https://www.openai.com)\\n\\n',\n    '[Link ',\n    'to Google',\n    'I](https://ww',\n    'w.google.com/1',\n    '11',\n    '22',\n    'xx',\n    'yy',\n    'zz',\n    'aa',\n    'bb',\n    'cc',\n    'dd',\n    'ee',\n    '33)\\n\\n',\n    '[False Positive',\n    '11',\n    '22',\n    '33',\n    '44',\n    '55',\n    '66',\n    '77',\n    '88',\n    '99',\n    '100',\n  ],\n  lists: ['1.', ' number 1\\n', '- ', 'List B\\n', '-', ' [x] ', 'Task C'],\n  listWithImage: [\n    '## ',\n    'Links ',\n    'and ',\n    'Images\\n\\n',\n    '- [Link ',\n    'to OpenA',\n    'I](https://www.openai.com)\\n',\n    '- ![Sample Image](https://via.placeholder.com/150)\\n\\n',\n  ],\n  nestedStructureBlock: [\n    '```',\n    'javascript',\n    '\\n',\n    'import',\n    ' React',\n    ' from',\n    \" '\",\n    'react',\n    \"';\\n\",\n    'import',\n    ' {',\n    ' Plate',\n    ' }',\n    ' from',\n    \" '@\",\n    'ud',\n    'ecode',\n    '/',\n    'plate',\n    \"';\\n\\n\",\n    'const',\n    ' Basic',\n    'Editor',\n    ' =',\n    ' ()',\n    ' =>',\n    ' {\\n',\n    ' ',\n    ' return',\n    ' (\\n',\n    '   ',\n    ' <',\n    'Plate',\n    '>\\n',\n    '     ',\n    ' {/*',\n    ' Add',\n    ' your',\n    ' plugins',\n    ' and',\n    ' components',\n    ' here',\n    ' */}\\n',\n    '   ',\n    ' </',\n    'Plate',\n    '>\\n',\n    ' ',\n    ' );\\n',\n    '};\\n\\n',\n    'export',\n    ' default',\n    ' Basic',\n    'Editor',\n    ';\\n',\n    '```',\n  ],\n  table: [\n    '| Feature          |',\n    ' Plate',\n    '.js',\n    '                                     ',\n    ' ',\n    '| Slate.js                                     ',\n    ' ',\n    '|\\n|------------------',\n    '|--------------------------------',\n    '---------------',\n    '|--------------------------------',\n    '---------------',\n    '|\\n| Purpose         ',\n    ' ',\n    '| Rich text editor framework',\n    '                   ',\n    ' ',\n    '| Rich text editor framework',\n    '                   ',\n    ' ',\n    '|\\n| Flexibility     ',\n    ' ',\n    '| Highly customizable',\n    ' with',\n    ' plugins',\n    '             ',\n    ' ',\n    '| Highly customizable',\n    ' with',\n    ' plugins',\n    '             ',\n    ' ',\n    '|\\n| Community       ',\n    ' ',\n    '| Growing community support',\n    '                    ',\n    ' ',\n    '| Established community',\n    ' support',\n    '                ',\n    ' ',\n    '|\\n| Documentation   ',\n    ' ',\n    '| Comprehensive documentation',\n    ' available',\n    '        ',\n    ' ',\n    '| Comprehensive documentation',\n    ' available',\n    '        ',\n    ' ',\n    '|\\n| Performance     ',\n    ' ',\n    '| Optimized for performance',\n    ' with',\n    ' large',\n    ' documents',\n    '| Good performance, but',\n    ' may',\n    ' require',\n    ' optimization',\n    '|\\n| Integration     ',\n    ' ',\n    '| Easy integration with',\n    ' React',\n    '                  ',\n    ' ',\n    '| Easy integration with',\n    ' React',\n    '                  ',\n    ' ',\n    '|\\n| Use Cases       ',\n    ' ',\n    '| Suitable for complex',\n    ' editing',\n    ' needs',\n    '           ',\n    ' ',\n    '| Suitable for complex',\n    ' editing',\n    ' needs',\n    '           ',\n    ' ',\n    '\\n\\n',\n    'Paragraph ',\n    'should ',\n    'exist ',\n    'from ',\n    'table',\n  ],\n};\n\nexport default function MarkdownStreamingDemo() {\n  const [selectedScenario, setSelectedScenario] =\n    useState<keyof typeof testScenarios>('columns');\n  const [activeIndex, setActiveIndex] = useState<number>(0);\n  const pausedRef = useRef(false);\n  const [paused, setPaused] = useState(false);\n  const streamSessionRef = useRef(0);\n  const [, forceUpdate] = useReducer((x) => x + 1, 0);\n  const [streaming, setStreaming] = useState(false);\n  const [isPlateStatic, setIsPlateStatic] = useState(false);\n  const [speed, setSpeed] = useState<number | null>(null);\n\n  const editor = usePlateEditor(\n    {\n      plugins: [...CopilotKit, ...EditorKit],\n      value: [],\n    },\n    []\n  );\n\n  const editorStatic = usePlateViewEditor(\n    {\n      plugins: BaseEditorKit,\n    },\n    []\n  );\n\n  const currentChunks = testScenarios[selectedScenario];\n  const transformedCurrentChunks = transformedChunks(currentChunks);\n\n  const setPausedState = useCallback((nextPaused: boolean) => {\n    pausedRef.current = nextPaused;\n    setPaused(nextPaused);\n  }, []);\n\n  const onStreaming = useCallback(async () => {\n    setStreaming(true);\n    streamSessionRef.current += 1;\n    const sessionId = streamSessionRef.current;\n\n    setPausedState(false);\n    setActiveIndex(0);\n    // editor.tf.setValue([]);\n\n    editor.setOption(AIChatPlugin, 'streaming', false);\n    editor.setOption(AIChatPlugin, '_blockChunks', '');\n    editor.setOption(AIChatPlugin, '_blockPath', null);\n\n    for (let i = 0; i < transformedCurrentChunks.length; i++) {\n      while (pausedRef.current) {\n        if (sessionId !== streamSessionRef.current) return;\n        await new Promise((resolve) => setTimeout(resolve, 100));\n      }\n\n      if (sessionId !== streamSessionRef.current) return;\n\n      setActiveIndex(i + 1);\n\n      const chunk = transformedCurrentChunks[i];\n\n      streamInsertChunk(editor, chunk.chunk, {\n        textProps: {\n          [getPluginType(editor, KEYS.ai)]: true,\n        },\n      });\n\n      await new Promise((resolve) =>\n        setTimeout(resolve, speed ?? chunk.delayInMs)\n      );\n\n      if (sessionId !== streamSessionRef.current) return;\n    }\n    setStreaming(false);\n  }, [editor, transformedCurrentChunks, speed, setPausedState]);\n\n  // eslint-disable-next-line react-hooks/immutability -- Static editor demo intentionally mutates editor.children during playback.\n  const onStreamingStatic = useCallback(async () => {\n    let output = '';\n    setStreaming(true);\n    streamSessionRef.current += 1;\n    const sessionId = streamSessionRef.current;\n    setPausedState(false);\n\n    for (const chunk of transformedCurrentChunks) {\n      while (pausedRef.current) {\n        if (sessionId !== streamSessionRef.current) return;\n        await new Promise((resolve) => setTimeout(resolve, 100));\n      }\n\n      if (sessionId !== streamSessionRef.current) return;\n\n      output += chunk.chunk;\n      // eslint-disable-next-line react-hooks/immutability -- Demo component intentionally mutates static editor for streaming visualization.\n      editorStatic.children = deserializeMd(editorStatic, output);\n      setActiveIndex((prev) => prev + 1);\n      forceUpdate();\n      await new Promise((resolve) =>\n        setTimeout(resolve, speed ?? chunk.delayInMs)\n      );\n    }\n    setStreaming(false);\n  }, [editorStatic, speed, transformedCurrentChunks, setPausedState]);\n\n  const onReset = useCallback(() => {\n    setActiveIndex(0);\n    if (isPlateStatic) {\n      // eslint-disable-next-line react-hooks/immutability -- Demo component intentionally mutates static editor for reset.\n      editorStatic.children = [];\n      forceUpdate();\n    } else {\n      editor.tf.setValue([]);\n      editor.setOption(AIChatPlugin, 'streaming', false);\n      editor.setOption(AIChatPlugin, '_blockChunks', '');\n      editor.setOption(AIChatPlugin, '_blockPath', null);\n    }\n  }, [editor, editorStatic, isPlateStatic]);\n\n  const onNavigate = useCallback(\n    (targetIndex: number) => {\n      // Check if navigation is possible\n      if (targetIndex < 0 || targetIndex > transformedCurrentChunks.length)\n        return;\n\n      if (isPlateStatic) {\n        let output = '';\n        for (const chunk of transformedCurrentChunks.slice(0, targetIndex)) {\n          output += chunk.chunk;\n        }\n\n        // eslint-disable-next-line react-hooks/immutability -- Demo component intentionally mutates static editor for navigation.\n        editorStatic.children = deserializeMd(editorStatic, output);\n        setActiveIndex(targetIndex);\n        forceUpdate();\n      } else {\n        editor.tf.setValue([]);\n\n        editor.setOption(AIChatPlugin, 'streaming', false);\n        editor.setOption(AIChatPlugin, '_blockChunks', '');\n        editor.setOption(AIChatPlugin, '_blockPath', null);\n\n        for (const chunk of transformedCurrentChunks.slice(0, targetIndex)) {\n          streamInsertChunk(editor, chunk.chunk, {\n            textProps: {\n              [getPluginType(editor, KEYS.ai)]: true,\n            },\n          });\n        }\n        setActiveIndex(targetIndex);\n      }\n    },\n    [editor, editorStatic, isPlateStatic, transformedCurrentChunks]\n  );\n\n  const onPrev = useCallback(\n    () => onNavigate(activeIndex - 1),\n    [onNavigate, activeIndex]\n  );\n  const onNext = useCallback(\n    () => onNavigate(activeIndex + 1),\n    [onNavigate, activeIndex]\n  );\n\n  return (\n    <section className=\"h-full overflow-y-auto p-20\">\n      <div className=\"mb-10 rounded bg-gray-100 p-4\">\n        {/* Scenario Selection */}\n        <div className=\"mb-4\">\n          <span className=\"mb-2 block font-medium text-sm\">Test Scenario:</span>\n          <select\n            className=\"w-64 rounded border px-3 py-2\"\n            value={selectedScenario}\n            onChange={(e) => {\n              setSelectedScenario(e.target.value as keyof typeof testScenarios);\n              setActiveIndex(0);\n              editor.tf.setValue([]);\n            }}\n          >\n            {Object.entries(testScenarios).map(([key]) => (\n              <option key={key} value={key}>\n                {key\n                  .replace(CAPITALIZE_REGEX, ' $1')\n                  .replace(FIRST_CHAR_REGEX, (str) => str.toUpperCase())}\n              </option>\n            ))}\n          </select>\n        </div>\n\n        {/* Control Buttons */}\n        <div className=\"mb-4 flex items-center gap-2\">\n          <Button onClick={onPrev}>\n            <ChevronFirstIcon />\n          </Button>\n\n          <Button\n            // eslint-disable-next-line react-hooks/immutability -- Static editor playback mutates editor.children through onStreamingStatic.\n            onClick={() => {\n              if (streaming) {\n                setPausedState(!pausedRef.current);\n              } else if (isPlateStatic) {\n                onStreamingStatic();\n              } else {\n                onStreaming();\n              }\n            }}\n          >\n            {paused || !streaming ? <PlayIcon /> : <PauseIcon />}\n          </Button>\n\n          <Button onClick={onNext}>\n            <ChevronLastIcon />\n          </Button>\n\n          <Button onClick={() => onReset()}>\n            <RotateCcwIcon />\n          </Button>\n\n          <Button\n            onClick={() => {\n              setIsPlateStatic(!isPlateStatic);\n              onReset();\n            }}\n          >\n            Switch to {isPlateStatic ? 'Plate' : 'PlateStatic'}\n          </Button>\n        </div>\n\n        <div className=\"mb-4 flex items-center gap-2\">\n          <span className=\"block font-medium text-sm\">Speed:</span>\n          <select\n            className=\"rounded border px-2 py-1\"\n            value={speed ?? 'default'}\n            onChange={(e) =>\n              setSpeed(\n                e.target.value === 'default' ? null : Number(e.target.value)\n              )\n            }\n          >\n            {['default', 10, 100, 200].map((ms) => (\n              <option key={ms} value={ms}>\n                {ms === 'default'\n                  ? 'Default'\n                  : ms === 10\n                    ? 'Fast(10ms)'\n                    : ms === 100\n                      ? 'Medium(100ms)'\n                      : ms === 200\n                        ? 'Slow(200ms)'\n                        : `${ms}ms`}\n              </option>\n            ))}\n          </select>\n          <span className=\"text-muted-foreground text-sm\">\n            The default speed is 10ms, but it adjusts to 100ms when streaming a\n            table or code block.\n          </span>\n        </div>\n\n        <div className=\"my-4 h-2 w-full rounded bg-gray-200\">\n          <div\n            className=\"h-2 rounded bg-primary transition-all duration-300\"\n            style={{\n              width: `${(activeIndex / (transformedCurrentChunks.length || 1)) * 100}%`,\n            }}\n          />\n        </div>\n\n        <span className=\"text-muted-foreground text-sm\">\n          PlateStatic offers more robust and flawless performance.\n        </span>\n      </div>\n\n      <div className=\"my-2 flex gap-10\">\n        <div className=\"w-1/2\">\n          <h3 className=\"mb-2 font-semibold\">\n            Transformed Chunks ({activeIndex}/{transformedCurrentChunks.length})\n          </h3>\n          <Tokens\n            activeIndex={activeIndex}\n            chunkClick={onNavigate}\n            chunks={splitChunksByLinebreak(\n              transformedCurrentChunks.map((c) => c.chunk)\n            )}\n          />\n        </div>\n\n        <div className=\"w-1/2\">\n          <h3 className=\"mb-2 font-semibold\">Editor Output</h3>\n          {isPlateStatic ? (\n            <EditorView\n              className=\"h-[500px] overflow-y-auto rounded border\"\n              editor={editorStatic}\n            />\n          ) : (\n            <Plate editor={editor}>\n              <EditorContainer className=\"h-[500px] overflow-y-auto rounded border\">\n                <Editor\n                  variant=\"demo\"\n                  className=\"pb-[20vh]\"\n                  placeholder=\"Type something...\"\n                  spellCheck={false}\n                />\n              </EditorContainer>\n            </Plate>\n          )}\n        </div>\n      </div>\n\n      <h2 className=\"mt-8 mb-4 font-semibold text-xl\">Raw Token Comparison</h2>\n      <div className=\"my-2 flex gap-10\">\n        <div className=\"w-1/2\">\n          <h3 className=\"mb-2 font-semibold\">Original Chunks</h3>\n          <Tokens\n            activeIndex={0}\n            chunks={splitChunksByLinebreak(currentChunks)}\n          />\n        </div>\n\n        <div className=\"w-1/2\">\n          <h3 className=\"mb-2 font-semibold\">Raw Markdown Text</h3>\n          <textarea\n            className={cn(\n              'h-[500px] w-full overflow-y-auto rounded border p-4 font-mono text-sm'\n            )}\n            readOnly\n            value={currentChunks.join('')}\n          />\n        </div>\n      </div>\n    </section>\n  );\n}\n\ntype TChunks = {\n  chunks: {\n    index: number;\n    text: string;\n  }[];\n  linebreaks: number;\n};\n\nfunction splitChunksByLinebreak(chunks: string[]) {\n  const result: TChunks[] = [];\n  let current: { index: number; text: string }[] = [];\n\n  for (let i = 0; i < chunks.length; i++) {\n    const chunk = chunks[i];\n    current.push({ index: i, text: chunk });\n\n    const match = TRAILING_NEWLINES_REGEX.exec(chunk);\n    if (match) {\n      const linebreaks = match[1].length;\n      result.push({\n        chunks: [...current],\n        linebreaks,\n      });\n      current = [];\n    }\n  }\n\n  if (current.length > 0) {\n    result.push({\n      chunks: [...current],\n      linebreaks: 0,\n    });\n  }\n\n  return result;\n}\n\ntype TChunk = { chunk: string; delayInMs: number };\nconst transformedChunks = (chunks: string[]): TChunk[] => {\n  const result: TChunk[] = [];\n  const joiner = new MarkdownJoiner();\n  for (const chunk of chunks) {\n    const processed = joiner.processText(chunk);\n    if (processed) {\n      result.push({ chunk: processed, delayInMs: joiner.delayInMs });\n    }\n  }\n  // flush any remaining buffered content\n  const remaining = joiner.flush();\n  if (remaining) {\n    result.push({ chunk: remaining, delayInMs: joiner.delayInMs });\n  }\n  return result;\n};\n\nconst Tokens = ({\n  activeIndex,\n  chunkClick,\n  chunks,\n  ...props\n}: {\n  activeIndex: number;\n  chunks: TChunks[];\n  chunkClick?: (index: number) => void;\n} & HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className=\"my-1 h-[500px] overflow-y-auto rounded bg-gray-100 p-4 font-mono\"\n    {...props}\n  >\n    {chunks.map((chunk, index) => (\n      <div key={index} className=\"py-1\">\n        {chunk.chunks.map((c, j) => {\n          const lineBreak = c.text.replaceAll('\\n', '⤶');\n          const space = lineBreak.replaceAll(' ', '␣');\n\n          return (\n            <span\n              key={j}\n              role=\"button\"\n              className={cn(\n                'mx-1 inline-block rounded border p-1',\n                activeIndex && c.index < activeIndex && 'bg-amber-400'\n              )}\n              onClick={() => chunkClick?.(c.index + 1)}\n            >\n              {space}\n            </span>\n          );\n        })}\n      </div>\n    ))}\n  </div>\n);\n",
      "type": "registry:example"
    }
  ],
  "type": "registry:example"
}