{"_id":"@ai-markdown/react","_rev":"4-f1ad7bc1676881a6ad2847dcb0893930","name":"@ai-markdown/react","dist-tags":{"latest":"3.0.0","beta":"3.0.0-beta.2","rc":"3.0.0-rc.1"},"versions":{"3.0.0-beta.1":{"name":"@ai-markdown/react","version":"3.0.0-beta.1","keywords":["react","react-component","markdown","markdown-renderer","ai","llm","chatgpt","chat","chatbot","streaming","streaming-markdown","typewriter","latex","katex","math","gfm","github-flavored-markdown","footnotes","mermaid","cjk","chinese","syntax-highlighting","remark","rehype","unified","incremental-parsing","typescript","ssr"],"author":{"url":"https://github.com/AIEPhoenix","name":"Brian Lee","email":"aiephoenixbl@gmail.com"},"license":"MIT","_id":"@ai-markdown/react@3.0.0-beta.1","maintainers":[{"name":"aiephoenix","email":"aiephoenixbl@gmail.com"}],"homepage":"https://github.com/ai-markdown/ai-markdown/tree/main/packages/react#readme","bugs":{"url":"https://github.com/ai-markdown/ai-markdown/issues"},"dist":{"shasum":"38708c580d2e7990b0ac015349081df995c41efd","tarball":"https://registry.npmjs.org/@ai-markdown/react/-/react-3.0.0-beta.1.tgz","fileCount":28,"integrity":"sha512-xk/8ciXuyp5eykFiof5wAv9JDS9SP8lzIxLIZzm17kMioEXWADdbw3Z7dhrEiitqRJHhZebJIqxMmFmeUTmAmA==","signatures":[{"sig":"MEQCIDHVTMYoc4rBJD1sdScog/EI4+28uuoX2XQurbySDkuuAiAqFnMZvsRTRTOpczW5vAT/0LhzKMXzJkfbwjRPHYGDAA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@ai-markdown%2freact@3.0.0-beta.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":2149343},"main":"./dist/index.cjs","type":"module","_from":"file:/home/runner/work/_temp/first-publish-packs/ai-markdown-react-3.0.0-beta.1.tgz","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=20"},"exports":{".":{"types":{"import":"./dist/index.d.ts","require":"./dist/index.d.cts"},"import":"./dist/index.js","require":"./dist/index.cjs","development":{"import":"./dist/index.dev.js","require":"./dist/index.dev.cjs"}},"./plugins":{"types":{"import":"./dist/plugins/index.d.ts","require":"./dist/plugins/index.d.cts"},"import":"./dist/plugins/index.js","require":"./dist/plugins/index.cjs","development":{"import":"./dist/plugins/index.dev.js","require":"./dist/plugins/index.dev.cjs"}},"./package.json":"./package.json","./typography/*.css":"./dist/typography/*.css"},"scripts":{"test":"vitest --run","build":"pnpm run build:js && pnpm run build:css","build:js":"node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsup && node scripts/assert-dist-clean.mjs && node scripts/assert-prod-diagnostic-inert.mjs","build:css":"sass src/components/typography/variants/:dist/typography/ && postcss dist/typography/*.css --replace","typecheck":"tsc --noEmit -p tsconfig.json"},"_npmUser":{"name":"aiephoenix","email":"aiephoenixbl@gmail.com"},"_resolved":"/home/runner/work/_temp/first-publish-packs/ai-markdown-react-3.0.0-beta.1.tgz","_integrity":"sha512-xk/8ciXuyp5eykFiof5wAv9JDS9SP8lzIxLIZzm17kMioEXWADdbw3Z7dhrEiitqRJHhZebJIqxMmFmeUTmAmA==","repository":{"url":"git+https://github.com/ai-markdown/ai-markdown.git","type":"git","directory":"packages/react"},"_npmVersion":"10.9.8","description":"React rendering, streaming components and hooks for ai-markdown.","directories":{},"sideEffects":["**/*.css"],"_nodeVersion":"22.23.2","dependencies":{"@types/hast":"^3.0.5","unist-util-visit":"^5.1.0","@ai-markdown/core":"3.0.0-beta.1","@ai-markdown/engine":"3.0.0-beta.1","hast-util-to-jsx-runtime":"^2.3.6"},"publishConfig":{"access":"public"},"typesVersions":{"*":{"plugins":["./dist/plugins/index.d.ts"]}},"_hasShrinkwrap":false,"devDependencies":{"react":"^19","vfile":"^6.0.3","unified":"^11.0.5","lodash-es":"^4.18.0","react-dom":"^19","remark-gfm":"^4.0.1","remark-toc":"^9.0.0","@types/node":"^25.9.5","remark-math":"^6.0.0","@types/mdast":"^4.0.4","@types/react":"^19.2.18","rehype-katex":"^7.0.1","remark-emoji":"^5.0.2","remark-pangu":"^2.2.0","remark-parse":"^11.0.0","remark-breaks":"^4.0.0","remark-rehype":"^11.1.2","rehype-sanitize":"^6.0.0","@types/lodash-es":"^4.17.12","@types/react-dom":"^19.2.7","remark-smartypants":"^3.0.3","remark-cjk-friendly":"^2.3.1","rehype-unwrap-images":"^1.0.0","remark-definition-list":"^2.0.0","remark-remove-comments":"^1.1.1","@ai-markdown/rehype-raw":"7.0.3","remark-squeeze-paragraphs":"^6.0.0","@ai-markdown/remark-mark-highlight":"^1.0.1","remark-cjk-friendly-gfm-strikethrough":"^2.3.1"},"peerDependencies":{"katex":"^0.16.0 || ^0.17.0","react":"^19.0.0","react-dom":"^19.0.0"},"peerDependenciesMeta":{"katex":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/react_3.0.0-beta.1_1788928853231_0.6326253207037253","host":"s3://npm-registry-packages-npm-production"}},"3.0.0-beta.2":{"name":"@ai-markdown/react","version":"3.0.0-beta.2","keywords":["react","react-component","markdown","markdown-renderer","ai","llm","chatgpt","chat","chatbot","streaming","streaming-markdown","typewriter","latex","katex","math","gfm","github-flavored-markdown","footnotes","mermaid","cjk","chinese","syntax-highlighting","remark","rehype","unified","incremental-parsing","typescript","ssr"],"author":"Brian Lee <aiephoenixbl@gmail.com> (https://github.com/AIEPhoenix)","license":"MIT","_id":"@ai-markdown/react@3.0.0-beta.2","maintainers":[{"name":"aiephoenix","email":"aiephoenixbl@gmail.com"}],"homepage":"https://github.com/ai-markdown/ai-markdown/tree/main/packages/react#readme","bugs":{"url":"https://github.com/ai-markdown/ai-markdown/issues"},"dist":{"shasum":"9e6bf02ca125be635d70a2c08410a01fcf011ba6","tarball":"https://registry.npmjs.org/@ai-markdown/react/-/react-3.0.0-beta.2.tgz","fileCount":28,"integrity":"sha512-Okdlb464Cm16HuS51LboJY+bEGlOfJqjTuC9nw+3OAnlBcldoKHjBWvSOqQdVoQmpg0zwd9EnK1afKA1CPF6FQ==","signatures":[{"sig":"MEQCICg9YqEZo9DzsIkxdc+CKQq0MdisRDruuOeHi4WhHe44AiBoYoWrk+jvF3wUsy5ORMqPwpfoqcjajKcUukd4nl4v0Q==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEYCIQCj79DBFmsNsAdw0rUwe6prso/j2MQ7SNWD41exIYP4sAIhAIoHECa8saxKAsy597+qXAItdSQqjm5vIKkgGGt23sY2","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@ai-markdown%2freact@3.0.0-beta.2","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":2149695},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=20"},"exports":{".":{"types":{"import":"./dist/index.d.ts","require":"./dist/index.d.cts"},"import":"./dist/index.js","require":"./dist/index.cjs","development":{"import":"./dist/index.dev.js","require":"./dist/index.dev.cjs"}},"./plugins":{"types":{"import":"./dist/plugins/index.d.ts","require":"./dist/plugins/index.d.cts"},"import":"./dist/plugins/index.js","require":"./dist/plugins/index.cjs","development":{"import":"./dist/plugins/index.dev.js","require":"./dist/plugins/index.dev.cjs"}},"./package.json":"./package.json","./typography/*.css":"./dist/typography/*.css"},"scripts":{"test":"vitest --run","build":"pnpm run build:js && pnpm run build:css","build:js":"node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsup && node scripts/assert-dist-clean.mjs && node scripts/assert-prod-diagnostic-inert.mjs","build:css":"sass src/components/typography/variants/:dist/typography/ && postcss dist/typography/*.css --replace","typecheck":"tsc --noEmit -p tsconfig.json"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:f0699dfd-98b7-4b29-8300-0655faa75d19"}},"repository":{"url":"git+https://github.com/ai-markdown/ai-markdown.git","type":"git","directory":"packages/react"},"description":"React rendering, streaming components and hooks for ai-markdown.","directories":{},"sideEffects":["**/*.css"],"_nodeVersion":"26.0.0","dependencies":{"@types/hast":"^3.0.5","unist-util-visit":"^5.1.0","@ai-markdown/core":"3.0.0-beta.2","@ai-markdown/engine":"3.0.0-beta.2","hast-util-to-jsx-runtime":"^2.3.6"},"publishConfig":{"access":"public"},"typesVersions":{"*":{"plugins":["./dist/plugins/index.d.ts"]}},"_hasShrinkwrap":false,"devDependencies":{"react":"^19","vfile":"^6.0.3","unified":"^11.0.5","lodash-es":"^4.18.0","react-dom":"^19","remark-gfm":"^4.0.1","remark-toc":"^9.0.0","@types/node":"^25.9.5","remark-math":"^6.0.0","@types/mdast":"^4.0.4","@types/react":"^19.2.18","rehype-katex":"^7.0.1","remark-emoji":"^5.0.2","remark-pangu":"^2.2.0","remark-parse":"^11.0.0","remark-breaks":"^4.0.0","remark-rehype":"^11.1.2","rehype-sanitize":"^6.0.0","@types/lodash-es":"^4.17.12","@types/react-dom":"^19.2.7","remark-smartypants":"^3.0.3","remark-cjk-friendly":"^2.3.1","rehype-unwrap-images":"^1.0.0","remark-definition-list":"^2.0.0","remark-remove-comments":"^1.1.1","@ai-markdown/rehype-raw":"7.0.3","remark-squeeze-paragraphs":"^6.0.0","@ai-markdown/remark-mark-highlight":"^1.0.1","remark-cjk-friendly-gfm-strikethrough":"^2.3.1"},"peerDependencies":{"katex":"^0.16.0 || ^0.17.0","react":"^19.0.0","react-dom":"^19.0.0"},"peerDependenciesMeta":{"katex":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/react_3.0.0-beta.2_1788963003807_0.4918677554498667","host":"s3://npm-registry-packages-npm-production"}},"3.0.0-rc.1":{"name":"@ai-markdown/react","version":"3.0.0-rc.1","keywords":["react","react-component","markdown","markdown-renderer","ai","llm","chatgpt","chat","chatbot","streaming","streaming-markdown","typewriter","latex","katex","math","gfm","github-flavored-markdown","footnotes","mermaid","cjk","chinese","syntax-highlighting","remark","rehype","unified","incremental-parsing","typescript","ssr"],"author":"Brian Lee <aiephoenixbl@gmail.com> (https://github.com/AIEPhoenix)","license":"MIT","_id":"@ai-markdown/react@3.0.0-rc.1","maintainers":[{"name":"aiephoenix","email":"aiephoenixbl@gmail.com"}],"homepage":"https://github.com/ai-markdown/ai-markdown/tree/main/packages/react#readme","bugs":{"url":"https://github.com/ai-markdown/ai-markdown/issues"},"dist":{"shasum":"d452bc77ba390e089b8a5394f97e9b2755e5a52a","tarball":"https://registry.npmjs.org/@ai-markdown/react/-/react-3.0.0-rc.1.tgz","fileCount":28,"integrity":"sha512-oz6ah0N0fGaGjO943hOoqLCxu3pNZ0LzeIvVUgD8q0I+AkuPXEyQSBcUhjdhFDtxoHjOXLMpgo9MX6Bq/EVH9A==","signatures":[{"sig":"MEUCIAmprdDqRH9nmUU5HBKRDoB1A+3uDp/nslJD6fDQ/zetAiEA/hGf0aTke7rdE+eBBEQn7jRTSDrq9ggui1cREgx5GBA=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEQCIDQZEqc3HGPHysIigzS57bHYfx1EZlUb4wSJXF00q6nDAiBnFFT2I1Dp0bx1yJrmBj7HN9iL/MFphEElB/vmaoscOg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@ai-markdown%2freact@3.0.0-rc.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":2149620},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":"^20.19.0 || >=22.12.0"},"exports":{".":{"types":{"import":"./dist/index.d.ts","require":"./dist/index.d.cts"},"import":"./dist/index.js","require":"./dist/index.cjs","development":{"import":"./dist/index.dev.js","require":"./dist/index.dev.cjs"}},"./plugins":{"types":{"import":"./dist/plugins/index.d.ts","require":"./dist/plugins/index.d.cts"},"import":"./dist/plugins/index.js","require":"./dist/plugins/index.cjs","development":{"import":"./dist/plugins/index.dev.js","require":"./dist/plugins/index.dev.cjs"}},"./package.json":"./package.json","./typography/*.css":"./dist/typography/*.css"},"scripts":{"test":"vitest --run","build":"pnpm run build:js && pnpm run build:css","build:js":"node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsup && node scripts/assert-dist-clean.mjs && node scripts/assert-prod-diagnostic-inert.mjs","build:css":"sass src/components/typography/variants/:dist/typography/ && postcss dist/typography/*.css --replace","typecheck":"tsc --noEmit -p tsconfig.json"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:f0699dfd-98b7-4b29-8300-0655faa75d19"}},"repository":{"url":"git+https://github.com/ai-markdown/ai-markdown.git","type":"git","directory":"packages/react"},"description":"React rendering, streaming components and hooks for ai-markdown.","directories":{},"sideEffects":["**/*.css"],"_nodeVersion":"26.0.0","dependencies":{"@types/hast":"^3.0.5","unist-util-visit":"^5.1.0","@ai-markdown/core":"3.0.0-rc.1","@ai-markdown/engine":"3.0.0-rc.1","hast-util-to-jsx-runtime":"^2.3.6"},"publishConfig":{"access":"public"},"typesVersions":{"*":{"plugins":["./dist/plugins/index.d.ts"]}},"_hasShrinkwrap":false,"devDependencies":{"react":"^19","vfile":"^6.0.3","unified":"^11.0.5","lodash-es":"^4.18.0","react-dom":"^19","remark-gfm":"^4.0.1","remark-toc":"^9.0.0","@types/node":"^25.9.5","remark-math":"^6.0.0","@types/mdast":"^4.0.4","@types/react":"^19.2.18","rehype-katex":"^7.0.1","remark-emoji":"^5.0.2","remark-pangu":"^2.2.0","remark-parse":"^11.0.0","remark-breaks":"^4.0.0","remark-rehype":"^11.1.2","rehype-sanitize":"^6.0.0","@types/lodash-es":"^4.17.12","@types/react-dom":"^19.2.7","remark-smartypants":"^3.0.3","remark-cjk-friendly":"^2.3.1","rehype-unwrap-images":"^1.0.0","remark-definition-list":"^2.0.0","remark-remove-comments":"^1.1.1","@ai-markdown/rehype-raw":"7.0.3","remark-squeeze-paragraphs":"^6.0.0","@ai-markdown/storybook-kit":"0.0.0","@ai-markdown/remark-mark-highlight":"^1.0.2","remark-cjk-friendly-gfm-strikethrough":"^2.3.1"},"peerDependencies":{"katex":"^0.16.0 || ^0.17.0","react":"^19.0.0","react-dom":"^19.0.0"},"peerDependenciesMeta":{"katex":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/react_3.0.0-rc.1_1789052768282_0.787468946350814","host":"s3://npm-registry-packages-npm-production"}},"3.0.0":{"_id":"@ai-markdown/react@3.0.0","bugs":{"url":"https://github.com/ai-markdown/ai-markdown/issues"},"dist":{"shasum":"90a8b8792ec1323c0007d25ad899438385a9da5e","tarball":"https://registry.npmjs.org/@ai-markdown/react/-/react-3.0.0.tgz","fileCount":28,"integrity":"sha512-i6HhdgMgLR816Itv2B3EHYwwErb/CBqBfKzsrvs9d/7Lqf3TqIjNCfF+Wuur/QAFXQ37LZl3hXgrXDzY0OqPnA==","signatures":[{"sig":"MEQCIF7hkK+/wvPzF7gy3zuDiNnuMQ1sI8dhOb+cpFVvBFR+AiBgrDX8lRcmtcDZDwRMq/omuwM9SRrQ783Phugk3jJaAw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDAQZcrX51LBAzdJV3KZYbJ8eSzSczsMuYrJiv+D5/1EQIgSBmnCwkwqdne6kFygsXQ2ZOn2j4HdlzUlm5Ce/hKjRs="}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@ai-markdown%2freact@3.0.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":2149509},"main":"./dist/index.cjs","name":"@ai-markdown/react","type":"module","types":"./dist/index.d.ts","author":"Brian Lee <aiephoenixbl@gmail.com> (https://github.com/AIEPhoenix)","module":"./dist/index.js","engines":{"node":"^20.19.0 || >=22.12.0"},"exports":{".":{"types":{"import":"./dist/index.d.ts","require":"./dist/index.d.cts"},"import":"./dist/index.js","require":"./dist/index.cjs","development":{"import":"./dist/index.dev.js","require":"./dist/index.dev.cjs"}},"./plugins":{"types":{"import":"./dist/plugins/index.d.ts","require":"./dist/plugins/index.d.cts"},"import":"./dist/plugins/index.js","require":"./dist/plugins/index.cjs","development":{"import":"./dist/plugins/index.dev.js","require":"./dist/plugins/index.dev.cjs"}},"./package.json":"./package.json","./typography/*.css":"./dist/typography/*.css"},"license":"MIT","scripts":{"test":"vitest --run","build":"pnpm run build:js && pnpm run build:css","build:js":"node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsup && node scripts/assert-dist-clean.mjs && node scripts/assert-prod-diagnostic-inert.mjs","build:css":"sass src/components/typography/variants/:dist/typography/ && postcss dist/typography/*.css --replace","typecheck":"tsc --noEmit -p tsconfig.json"},"version":"3.0.0","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:f0699dfd-98b7-4b29-8300-0655faa75d19"}},"homepage":"https://github.com/ai-markdown/ai-markdown/tree/main/packages/react#readme","keywords":["react","react-component","markdown","markdown-renderer","ai","llm","chatgpt","chat","chatbot","streaming","streaming-markdown","typewriter","latex","katex","math","gfm","github-flavored-markdown","footnotes","mermaid","cjk","chinese","syntax-highlighting","remark","rehype","unified","incremental-parsing","typescript","ssr"],"repository":{"url":"git+https://github.com/ai-markdown/ai-markdown.git","type":"git","directory":"packages/react"},"description":"React rendering, streaming components and hooks for ai-markdown.","directories":{},"maintainers":[{"name":"aiephoenix","email":"aiephoenixbl@gmail.com"}],"sideEffects":["**/*.css"],"_nodeVersion":"26.0.0","dependencies":{"@types/hast":"^3.0.5","unist-util-visit":"^5.1.0","@ai-markdown/core":"3.0.0","@ai-markdown/engine":"3.0.0","hast-util-to-jsx-runtime":"^2.3.6"},"publishConfig":{"access":"public"},"typesVersions":{"*":{"plugins":["./dist/plugins/index.d.ts"]}},"_hasShrinkwrap":false,"devDependencies":{"react":"^19","vfile":"^6.0.3","unified":"^11.0.5","lodash-es":"^4.18.0","react-dom":"^19","remark-gfm":"^4.0.1","remark-toc":"^9.0.0","@types/node":"^25.9.5","remark-math":"^6.0.0","@types/mdast":"^4.0.4","@types/react":"^19.2.18","rehype-katex":"^7.0.1","remark-emoji":"^5.0.2","remark-pangu":"^2.2.0","remark-parse":"^11.0.0","remark-breaks":"^4.0.0","remark-rehype":"^11.1.2","rehype-sanitize":"^6.0.0","@types/lodash-es":"^4.17.12","@types/react-dom":"^19.2.7","remark-smartypants":"^3.0.3","remark-cjk-friendly":"^2.3.1","rehype-unwrap-images":"^1.0.0","remark-definition-list":"^2.0.0","remark-remove-comments":"^1.1.1","@ai-markdown/rehype-raw":"7.0.3","remark-squeeze-paragraphs":"^6.0.0","@ai-markdown/storybook-kit":"0.0.0","@ai-markdown/remark-mark-highlight":"^1.0.2","remark-cjk-friendly-gfm-strikethrough":"^2.3.1"},"peerDependencies":{"katex":"^0.16.0 || ^0.17.0","react":"^19.0.0","react-dom":"^19.0.0"},"peerDependenciesMeta":{"katex":{"optional":true}},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/react_3.0.0_1789053944246_0.8620967454565833"}}},"time":{"created":"2026-09-09T04:40:52.974Z","modified":"2026-09-10T15:25:44.729Z","3.0.0-beta.1":"2026-09-09T04:40:53.373Z","3.0.0-beta.2":"2026-09-09T14:10:03.955Z","3.0.0-rc.1":"2026-09-10T15:06:08.378Z","3.0.0":"2026-09-10T15:25:44.374Z"},"bugs":{"url":"https://github.com/ai-markdown/ai-markdown/issues"},"author":"Brian Lee <aiephoenixbl@gmail.com> (https://github.com/AIEPhoenix)","license":"MIT","homepage":"https://github.com/ai-markdown/ai-markdown/tree/main/packages/react#readme","keywords":["react","react-component","markdown","markdown-renderer","ai","llm","chatgpt","chat","chatbot","streaming","streaming-markdown","typewriter","latex","katex","math","gfm","github-flavored-markdown","footnotes","mermaid","cjk","chinese","syntax-highlighting","remark","rehype","unified","incremental-parsing","typescript","ssr"],"repository":{"url":"git+https://github.com/ai-markdown/ai-markdown.git","type":"git","directory":"packages/react"},"description":"React rendering, streaming components and hooks for ai-markdown.","maintainers":[{"name":"aiephoenix","email":"aiephoenixbl@gmail.com"}],"readme":"# @ai-markdown/react\n\n[![@ai-markdown/react stable](https://img.shields.io/npm/v/@ai-markdown/react?label=npm&color=blue)](https://www.npmjs.com/package/@ai-markdown/react?activeTab=versions)\n[![@ai-markdown/react monthly downloads](https://img.shields.io/npm/dm/@ai-markdown/react?label=downloads%2Fmonth&color=blue)](https://www.npmjs.com/package/@ai-markdown/react)\n[![TypeScript declarations included](https://img.shields.io/badge/TypeScript-included-3178c6?logo=typescript&logoColor=white)](https://github.com/ai-markdown/ai-markdown/tree/main/packages/react)\n[![MIT license](https://img.shields.io/badge/license-MIT-blue)](https://github.com/ai-markdown/ai-markdown/blob/main/LICENSE)\n\n[![React 19](https://img.shields.io/badge/React-19-149eca?logo=react&logoColor=white)](#compatibility)\n\nReact 19 application adapter. For Vue 3.5, use [`@ai-markdown/vue`](../vue/README.md). For package selection, CSS setup and API differences, see [Getting started](../../docs/getting-started.md).\n\n> **3.0.0:** React and Vue adapters share the public `@ai-markdown/core` and `@ai-markdown/engine` packages. See the [migration guide](../../docs/framework-transition.md).\n\n`@ai-markdown/react` renders accumulated Markdown strings in React. It combines GFM, KaTeX math, CJK delimiter handling, optional typography transforms, and a verified incremental parsing path for append-heavy content. Use it with the built-in CSS or supply your own typography and element components.\n\nThe React adapter owns the React lifecycle, context hooks, document coordination, and cached element construction. Its exact-version engine dependency owns syntax processing. Code fences remain code text in the React adapter; syntax highlighting, JSON presentation, and rendered Mermaid diagrams are supplied by the Mantine package or your custom `pre` component. Start with the installation and quick start, then use the API tables to make each customization explicit.\n\n> **Upgrading from 1.x?** v2.0.0 removes the 1.x object-based `config` channel (and its integrator default channel) in favor of flat props, a sealed engine-plugin catalog, and five narrow hooks. See the [migration guide](https://github.com/ai-markdown/ai-markdown/blob/main/docs/migrating-to-v2.md) for the complete old → new mapping with before/after code.\n\n## Features\n\n- **GFM** -- tables, strikethrough, task lists, autolinks via `remark-gfm`\n- **LaTeX math** -- inline and display math rendered with KaTeX; smart preprocessing handles currency `$` signs, bracket delimiters (`\\[...\\]`, `\\(...\\)`), pipe escaping, and mhchem commands\n- **Emoji** -- shortcode support (`:smile:`) via `remark-emoji`\n- **CJK-friendly** -- CJK-aware emphasis/strikethrough parsing, optional pangu spacing, and configurable fonts; source line breaks still become `<br>`\n- **Extra syntax** -- highlight (`==text==`), definition lists\n- **Display optimizations** -- SmartyPants typography, pangu CJK spacing, HTML comment removal\n- **Streaming-aware** -- built-in `streaming` flag propagated via context for custom components\n- **Smooth streaming** -- `AIMarkdownSmoothStream` shell (and the `useSmoothStream` hook beneath it) reveals bursty token chunks as a steady grapheme-by-grapheme typewriter; see [docs/smooth-streaming.md](https://github.com/ai-markdown/ai-markdown/blob/main/docs/smooth-streaming.md)\n- **Customizable** -- swap typography, color scheme, individual markdown element renderers, and inject extra style wrappers\n- **Metadata context** -- pass arbitrary data to deeply nested custom components without prop drilling, isolated from render state to avoid unnecessary re-renders\n- **TypeScript** -- fully typed flat props plus a metadata generic (`AIMarkdownProps<TMetadata>`)\n\n## Package family\n\n| Package                                                                                                  | Role                                                                                                        | Version policy                                            |\n| -------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- |\n| [`@ai-markdown/core`](https://www.npmjs.com/package/@ai-markdown/core)                                   | Framework-independent sessions, block planning, contributions and smooth coordination                       | Release train; exact engine dependency                    |\n| [`@ai-markdown/react`](https://www.npmjs.com/package/@ai-markdown/react)                                 | The React renderer — `<AIMarkdown>`, `<AIMarkdownSmoothStream>`, `<AIMarkdownDocuments>`, hooks, providers  | Release train                                             |\n| [`@ai-markdown/vue`](https://www.npmjs.com/package/@ai-markdown/vue)                                     | Vue 3.5 renderer — components, scoped slots, SSR/hydration and smooth composables                           | Release train; exact core and engine dependencies         |\n| [`@ai-markdown/react-mantine`](https://www.npmjs.com/package/@ai-markdown/react-mantine)                 | Mantine UI bindings — themed typography, code-highlight tabs, Mermaid, color-scheme wiring                  | Release train; compatible React 3.x peer                  |\n| [`@ai-markdown/engine`](https://www.npmjs.com/package/@ai-markdown/engine)                               | Framework-agnostic engine — incremental parsing, LaTeX preprocessing, plugin pipeline, cross-chunk registry | Release train; pinned exactly by shared core and adapters |\n| [`@ai-markdown/remark-mark-highlight`](https://www.npmjs.com/package/@ai-markdown/remark-mark-highlight) | remark plugin for `==mark==` highlight syntax                                                               | Independent semver                                        |\n\n## Compatibility\n\n|                |                                                                                                                                                                                                                                                                                |\n| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| React          | ≥ 19 (`react`, `react-dom` peer dependencies)                                                                                                                                                                                                                                  |\n| KaTeX          | `^0.16` or `^0.17` (optional peer — only if you render math)                                                                                                                                                                                                                   |\n| Node           | `^20.19.0 \\|\\| >=22.12.0` (`engines.node`)                                                                                                                                                                                                                                     |\n| Module formats | ESM and CJS, TypeScript types for both, `sideEffects` declared                                                                                                                                                                                                                 |\n| Runtimes       | Browser, Node, edge/workers; server rendering via `renderToString`, and the bundle keeps its `\"use client\"` directive for React Server Components apps — see [Streaming & performance](https://github.com/ai-markdown/ai-markdown/blob/main/docs/streaming-and-performance.md) |\n| Bundling       | ESM/CJS artifacts and declared side effects; selecting fewer plugins disables their pipeline behavior, but does not guarantee their dependencies disappear from the bundle                                                                                                     |\n\n## Installation\n\n```bash\n# npm\nnpm install @ai-markdown/react\n\n# pnpm\npnpm add @ai-markdown/react\n\n# yarn\nyarn add @ai-markdown/react\n```\n\nThe React adapter declares both `@ai-markdown/core` and `@ai-markdown/engine` as ordinary dependencies, pinned to the same train version when packed (`3.0.0` in this checkout). Applications install `@ai-markdown/react`; the package manager resolves the shared layers automatically. Core owns sessions, planning and coordination, while engine owns parsing and tree algorithms. Adapter authors may depend on these layers directly and should keep their versions aligned. Exact pins reduce version mismatch; they do not guarantee a single module instance across arbitrary nested installations.\n\n### Peer Dependencies\n\n```json\n{\n  \"react\": \"^19.0.0\",\n  \"react-dom\": \"^19.0.0\",\n  \"katex\": \"^0.16.0 || ^0.17.0\"\n}\n```\n\n`katex` is optional. For a new application using the math examples, also run `pnpm add react@^19 react-dom@^19 katex`; declare KaTeX directly when importing its CSS.\n\n### CSS Dependencies\n\nFor LaTeX math rendering, include the KaTeX stylesheet:\n\n```tsx\nimport 'katex/dist/katex.min.css';\n```\n\n`katex` is declared as an **optional peer dependency** — by this package and by `@ai-markdown/engine`, which owns the `rehype-katex` pipeline step. It ships transitively via `rehype-katex`, so a hoisted installation may expose the import transitively. Declare it in your own app when importing its CSS, so resolution does not depend on hoisting or installer configuration:\n\n```bash\nnpm install katex\n```\n\nSkip the install only if you have no `import 'katex/…'` calls in your app and don't render math.\n\nFor the built-in default typography, include the typography CSS:\n\n```tsx\nimport '@ai-markdown/react/typography/default.css';\n// or import all typography variants at once:\nimport '@ai-markdown/react/typography/all.css';\n```\n\n## Quick Start\n\n```tsx\nimport AIMarkdown from '@ai-markdown/react';\nimport 'katex/dist/katex.min.css';\nimport '@ai-markdown/react/typography/default.css';\n\nfunction App() {\n  return <AIMarkdown content=\"Hello **world**! Math: $E = mc^2$\" />;\n}\n```\n\n### Streaming Example\n\n```tsx\nfunction StreamingChat({ content, isStreaming }: { content: string; isStreaming: boolean }) {\n  return <AIMarkdown content={content} streaming={isStreaming} colorScheme=\"dark\" />;\n}\n```\n\n## Props API Reference\n\n### `AIMarkdownProps<TMetadata>`\n\nAll configuration is **flat props** resolved once against shipped defaults. An explicitly passed prop (`v != null`) overrides the shipped default; an absent prop falls to the shipped default. Passing `null` counts as absent — this guards against serialization boundaries (RSC, persistence) materializing \"not passed\" as `null` and punching through defaults.\n\nThe table below is also the **prop-name registry**: flat props share one namespace across the React adapter and every wrapper layer, so wrapper authors must check it — plus the wrappers they extend (e.g. mantine adds `codeBlock`) — before naming a new prop. A collision is a compile error at the `extends` site for TS consumers but a silent override for plain-JS consumers.\n\n| Prop                       | Type                                | Default                | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |\n| -------------------------- | ----------------------------------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `content`                  | `string`                            | **(required)**         | Raw markdown content to render.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |\n| `streaming`                | `boolean`                           | `false`                | Whether content is actively being streamed (e.g. from an LLM).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |\n| `streamingCursor`          | `ComponentType`                     | `undefined`            | Streaming cursor slot. While `streaming === true`, the given component is rendered after the markdown content and unmounted when streaming stops. Pass the exported `AIMarkdownStreamingCursor` for the built-in inline cursor. Compared by identity — define at module scope. Definition-aware: while a footnote definition streams, the cursor follows the text into its footer entry; it hides for tails it cannot truthfully point at (a streaming link-reference definition, which renders nothing; a definition whose footer entry lives in another chunk under cross-chunk coordination).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |\n| `fontSize`                 | `number \\| string`                  | `'0.9375rem'`          | Base font size. Numbers are treated as pixels.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |\n| `variant`                  | `AIMarkdownVariant`                 | `'default'`            | Typography variant name.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |\n| `colorScheme`              | `AIMarkdownColorScheme`             | `'light'`              | Color scheme name (`'light'`, `'dark'`, or custom).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |\n| `metadata`                 | `TMetadata`                         | `undefined`            | Arbitrary data passed to custom components via a dedicated context. Deliberately never stabilized by the library — stabilization is the consumer's responsibility.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |\n| `contentPreprocessors`     | `AIMDContentPreprocessor[]`         | `undefined`            | Additional preprocessors run after the built-in LaTeX preprocessor. An optional `createRemendPreprocessor()` factory (streaming tail repair — unterminated `**bold`/`` `code `` render styled mid-stream) ships with the package; its effect is opt-in; actual bundle elimination depends on the emitted package and consumer bundler.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |\n| `customComponents`         | `AIMarkdownCustomComponents`        | `undefined`            | `react-markdown` component overrides for specific HTML elements.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |\n| `Typography`               | `AIMarkdownTypographyComponent`     | `DefaultTypography`    | Typography wrapper component.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |\n| `ExtraStyles`              | `AIMarkdownExtraStylesComponent`    | `undefined`            | Optional extra style wrapper rendered between typography and content.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |\n| `documentId`               | `string`                            | auto via `useId()`     | Stable id for the _logical markdown document_ this `<AIMarkdown>` is rendering. Used as the id namespace for clobberable attributes (`id`, hash hrefs) so two documents on the same page do not cross-link (footnote `[^1]` in message A won't scroll to `[^1]` in message B). When one document is split into chunks rendered by multiple `<AIMarkdown>` instances, pass the SAME `documentId` to every chunk so prefixes align. The value is passed through `encodeURIComponent` before being injected into HTML attributes, so any string is safe (React's `useId()` output, your own opaque ids, user-supplied UUIDs — even ill-formed UTF-16 from a string truncated mid-emoji, which is hashed into the prefix and warns in dev builds). Long ids (>16 chars, e.g. UUIDs) are hashed via MurmurHash3 to a short Base62 form **inside the rendered `id=\"…\"`/`href=\"#…\"` prefix only** to keep HTML compact; the `documentId` exposed by `useAIMarkdownDocument()` and registry keying via `useDocumentRegistry` stay raw, so deep linking and any consumer code reading `documentId` are unaffected. |\n| `documentIndex`            | `number`                            | mount order            | This chunk's position in the DOCUMENT, for instances sharing a `documentId` under `<AIMarkdownDocuments>`. Cross-chunk state (footnote numbering, which chunk renders the aggregate footer) follows registration order, which defaults to mount order — correct when chunks mount once in document order. Pass a stable ordinal when chunks can mount out of order or remount (a virtualized transcript that unmounts messages scrolled out of view re-registers them at the end when they scroll back). Ignored outside `<AIMarkdownDocuments>`. See [Cross-chunk Coordination](#chunks-that-mount-out-of-order-virtualized-lists).                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |\n| `urlTransform`             | `UrlTransform \\| null`              | `defaultUrlTransform`  | Override the URL allowlist applied to `href`, `src`, and similar attributes. The default mirrors GitHub: `http`, `https`, `irc`, `ircs`, `mailto`, `xmpp`. Pass a function defined at module scope (or memoized) to permit additional schemes — see [Custom URL Schemes and Sanitization](#custom-url-schemes-and-sanitization).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |\n| `sanitizeSchema`           | `SanitizeSchema`                    | library default        | Override the `rehype-sanitize` schema. Build with [`extendSanitizeSchema`](#custom-url-schemes-and-sanitization) so the library's cross-chunk tag and KaTeX className allowlists survive — hand-rolling silently drops them.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |\n| `enginePlugins`            | `readonly AIMarkdownEnginePlugin[]` | `defaultEnginePlugins` | Sealed engine-plugin selection — accepts React-exported plugin objects from `@ai-markdown/react/plugins` only. Absent → all five shipped plugins; passing an array replaces the set wholesale. See [Engine Plugins](#engine-plugins).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |\n| `blockMemo`                | `boolean`                           | `true`                 | Block-level memoization. Output-invariant in standalone rendering — flipping it changes no rendered byte; under `<AIMarkdownDocuments>` it is the path cross-chunk coordination runs on (`false` leaves cross-chunk refs literal). See [Behavior Props](#behavior-props).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |\n| `incrementalParse`         | `boolean`                           | `true`                 | Prefix-freeze incremental parsing for streaming. Output-invariant; effective only while `blockMemo` is `true`. See [Behavior Props](#behavior-props).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |\n| `preserveOrphanReferences` | `boolean`                           | `true`                 | Protect orphan footnote/link definitions in incomplete streaming documents. Affects output. See [Behavior Props](#behavior-props).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |\n\n## Engine Plugins\n\nOptional pipeline features are selected through the `enginePlugins` prop, which accepts **sealed plugin objects** exported from the `@ai-markdown/react/plugins` subpath. All five are enabled by default.\n\n| Plugin           | Description                                                                                             |\n| ---------------- | ------------------------------------------------------------------------------------------------------- |\n| `highlight`      | `==Highlight==` syntax support                                                                          |\n| `definitionList` | Definition list syntax ([PHP Markdown Extra](https://michelf.ca/projects/php-markdown/extra/#def-list)) |\n| `removeComments` | Strip HTML comments                                                                                     |\n| `smartypants`    | Typographic substitutions: curly quotes, em-dashes (`--`), ellipses (`...`)                             |\n| `pangu`          | Auto-insert spaces between CJK and half-width characters                                                |\n\n### Example: Selective Plugins\n\n```tsx\nimport AIMarkdown from '@ai-markdown/react';\nimport { highlight, smartypants } from '@ai-markdown/react/plugins';\n\nconst PLUGINS = [highlight, smartypants]; // module scope — stable reference\n\n<AIMarkdown content={markdown} enginePlugins={PLUGINS} />;\n```\n\nPassing an array **replaces the selection wholesale** (array-atomic semantics) — the example above enables only highlight and smartypants, disabling the other three. The recommended \"turn one off\" idiom:\n\n```tsx\nimport { defaultEnginePlugins, pangu } from '@ai-markdown/react/plugins';\n\nconst PLUGINS = defaultEnginePlugins.filter((p) => p !== pangu);\n```\n\nRules worth knowing:\n\n- Omitting `enginePlugins` means `defaultEnginePlugins` (all five).\n- Each plugin's position in the produced chain comes from its internal stage metadata; the order of your array is irrelevant. Duplicates are deduplicated with a dev warning.\n- The set is **sealed**: only engine constructs plugins (the incremental engine's boundary scanner must know every construct's syntax; open injection would void its verification record). Third-party content extension stays open through `contentPreprocessors` + `customComponents`.\n- Plugin objects are not serializable. For remote-config scenarios, store `plugin.name` strings (typed as `AIMarkdownEnginePluginName`) and map them back to the exported singletons at the edge.\n- The prop is deep-equal-stabilized as a backstop, but an inline array still pays one comparison per render — define the array at module scope.\n\n## Behavior Props\n\nThree flat boolean props control engine behavior:\n\n| Prop                       | Type      | Default | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |\n| -------------------------- | --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `blockMemo`                | `boolean` | `true`  | Enables block-level memoization: the renderer splits each document into per-block units and memoizes each block's React subtree by source identity, so unchanged blocks skip `toJsxRuntime` and React reconcile work during streaming. Output is byte-identical to the disabled path in standalone rendering. Cross-chunk coordination (`<AIMarkdownDocuments>`) is wired through this path only — with `blockMemo={false}` a wrapped chunk renders as if standalone (cross-chunk references stay literal). Set `blockMemo={false}` as an escape hatch for debugging standalone documents. |\n| `incrementalParse`         | `boolean` | `true`  | Prefix-freeze incremental parsing for streaming: when content grows by appends, the renderer freezes the stable document prefix at a verified-safe boundary and re-parses only the tail (83–94% less pipeline stage time on the benchmark payloads; footnotes and cross-chunk documents splice too). Output is deep-equal to a full parse — enforced by a per-frame splice-equivalence test suite. Effective only while `blockMemo` is `true`.                                                                                                                                             |\n| `preserveOrphanReferences` | `boolean` | `true`  | Protects orphan `[^x]: …` footnote definitions from being silently dropped by `mdast-util-to-hast` when no matching `[^x]` reference exists. Useful for streamed content where the reference may arrive in a later chunk. Inside `<AIMarkdownDocuments>`, the wrapper's `preserveOrphanReferences` prop overrides this prop unconditionally.                                                                                                                                                                                                                                               |\n\n```tsx\n<AIMarkdown content={markdown} blockMemo={false} incrementalParse={false} />\n```\n\n## Cross-chunk Coordination\n\nWhen a single logical markdown document is split across multiple\n`<AIMarkdown>` instances (chunked streaming for chat UIs, etc.), wrap\nthem in `<AIMarkdownDocuments>` and pass the SAME `documentId` to every\nchunk to coordinate footnotes, link references, and image references\nacross chunks:\n\n```tsx\nimport AIMarkdown, { AIMarkdownDocuments } from '@ai-markdown/react';\n\n<AIMarkdownDocuments>\n  {message.chunks.map((c, i) => (\n    <AIMarkdown key={i} content={c} documentId={message.id} />\n  ))}\n</AIMarkdownDocuments>;\n```\n\nWithout the wrapper, each `<AIMarkdown>` is independent — its\nreferences resolve only within its own content (current standalone\nbehavior).\n\n### Chunks that mount out of order (virtualized lists)\n\nCross-chunk state — footnote numbering, and which chunk renders the\naggregate footer — follows the order chunks **register** in, which by\ndefault is the order they mount. That is correct as long as each chunk\nmounts once, in document order.\n\nA virtualized transcript breaks that assumption: a message scrolled out of\nview unmounts, and scrolling back re-registers it _after_ the chunks that\nstayed mounted — so footnotes renumber and the footer moves. Pass\n`documentIndex` (any stable per-chunk ordinal — the message's index in your\nlist) and registration order stops depending on mount order:\n\n```tsx\n<AIMarkdownDocuments>\n  {message.chunks.map((c, i) => (\n    <AIMarkdown key={i} content={c} documentId={message.id} documentIndex={i} />\n  ))}\n</AIMarkdownDocuments>\n```\n\nThe prop is optional and changes nothing when every chunk mounts once in\norder, so existing code needs no update.\n\n### `<AIMarkdownDocuments>` Props\n\n| Prop                       | Type        | Default | Description                                                                                                                                                                                                                                                                                                                                                         |\n| -------------------------- | ----------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `preserveOrphanReferences` | `boolean`   | `true`  | Controls orphan-reference protection for every chunk under this wrapper. Unconditionally overrides each chunk's `preserveOrphanReferences` prop. Does not gate cross-chunk coordination itself (that's gated by wrapper + `documentId`).                                                                                                                            |\n| `smoothTurnTaking`         | `boolean`   | `true`  | Wrapper-level switch for smooth-stream turn-taking: when `true`, `<AIMarkdownSmoothStream>` chunks sharing this `documentId` type one at a time in mount order. `false` lets every chunk pace independently. See [smooth streaming → turn-taking](https://github.com/ai-markdown/ai-markdown/blob/main/docs/smooth-streaming.md#multi-chunk-documents-turn-taking). |\n| `children`                 | `ReactNode` | -       | The `<AIMarkdown>` instances to coordinate. Nesting `<AIMarkdownDocuments>` inside another `<AIMarkdownDocuments>` throws.                                                                                                                                                                                                                                          |\n\n### `useDocumentRegistry(documentId)`\n\nReturns the cross-chunk `Registry` for the given `documentId`, or\n`null` when called outside `<AIMarkdownDocuments>` or when\n`documentId` is empty. The `Registry` shape is exported and stable\nacross minor versions — use it when writing typed helpers that operate\non the cross-chunk registry directly.\n\n```tsx\nimport { useDocumentRegistry, type Registry } from '@ai-markdown/react';\n\nfunction MyHelper({ documentId }: { documentId: string }) {\n  const registry: Registry | null = useDocumentRegistry(documentId);\n  // null when no <AIMarkdownDocuments> ancestor — treat as \"run standalone\".\n}\n```\n\n## Custom URL Schemes and Sanitization\n\nBy default `<AIMarkdown>` only renders links and images whose URLs use the standard set of safe protocols (`http`, `https`, `irc`, `ircs`, `mailto`, `xmpp`). Anything else — `javascript:`, `data:`, or your own `myapp://` — is stripped. This protects against XSS in LLM-generated markdown but also means private application schemes are unreachable without configuration.\n\n### The Two-Gate Model\n\nSanitization runs in **two independent gates** (defense in depth):\n\n1. **`rehype-sanitize` schema** — runs first, inside the rehype plugin chain, and drops the URL when the protocol is not in the schema's per-attribute allowlist (`protocols.href`, `protocols.src`, `protocols.cite`).\n2. **`urlTransform`** — runs second, at render time during the hast traversal, on every URL-bearing attribute, that survived the schema; the default transform returns an empty string for a disallowed URL. A custom transform may also return null or undefined to omit the attribute. Called per-attribute with the attribute name (`'href'` / `'src'` / …) so key-aware transforms can discriminate (e.g. allow a scheme on `href` but not on `src` to block tracker pixels).\n\nFor a private scheme to render, **both gates must permit it**. Allowing only one is the most common pitfall.\n\n**Cross-chunk symmetry.** When `<AIMarkdown>` instances are wrapped in `<AIMarkdownDocuments>`, link/image references resolved across chunks (chunk A defines `[evil]: …`, chunk B writes `[click][evil]`) go through both gates as well — the same `urlTransform` and `sanitizeSchema` you pass to `<AIMarkdown>` apply at render time. The per-attribute key (`'href'` vs `'src'`) is honored: a key-aware policy that permits a scheme on `<a>` but not `<img>` will produce identical behavior whether the reference is in-chunk or cross-chunk.\n\n### Allowing a Custom Scheme\n\nDefine both gates at module scope so their reference identity is stable across renders (this keeps the per-block memo cache warm):\n\n```tsx\nimport AIMarkdown, { defaultUrlTransform, extendSanitizeSchema, type UrlTransform } from '@ai-markdown/react';\n\n// Gate 2: compose with the default so https/mailto/etc. still work.\nconst ALLOWED = /^myapp:/i;\nconst URL_TRANSFORM: UrlTransform = (url, key, node) =>\n  key === 'href' && ALLOWED.test(url) ? url : defaultUrlTransform(url, key, node);\n\n// Gate 1: allow the application scheme for links. Images keep their default policy.\nconst SCHEMA = extendSanitizeSchema((s) => {\n  s.protocols!.href!.push('myapp');\n});\n\nfunction App() {\n  return <AIMarkdown content={markdown} urlTransform={URL_TRANSFORM} sanitizeSchema={SCHEMA} />;\n}\n```\n\n### `extendSanitizeSchema((draft) => Schema | void)`\n\nHands you a deep clone of the library's default sanitize schema. Mutate it freely (the original singleton is never touched) or return a replacement object. Library invariants — cross-chunk coordination tags (`cross-chunk-link`, `cross-chunk-image`, `footnote-sup`), the KaTeX `math-inline` / `math-display` className allowlist, the `<mark>` allowance — survive untouched. **Hand-rolling a schema that doesn't spread these invariants silently breaks coordinated rendering**, which is why the helper is the recommended path.\n\n```tsx\nconst SCHEMA = extendSanitizeSchema((s) => {\n  (s.tagNames ??= []).push('my-widget'); // add a tag\n  s.protocols!.href!.push('myapp'); // permit a protocol\n  (s.attributes ??= {})['my-widget'] = ['dataId', 'dataMode']; // allow attributes\n  // No `return` needed — mutate-only is fine.\n});\n```\n\n**Footguns** (also documented in JSDoc):\n\n- Returning `null` is treated like returning nothing (the mutated draft is used).\n- Reassigning the local parameter (`s = { ... }`) does NOT replace the draft — JS only rebinds the local. Either mutate the original or `return` an explicit value.\n- Throwing inside the modifier propagates uncaught. Usually fine because the helper is called once at module load.\n\n### Reference Stability and the Cache\n\nBoth `urlTransform` and `sanitizeSchema` participate in the per-block memo cache, but they are stabilized **asymmetrically**:\n\n- **`urlTransform`** is tracked by identity only. A new function reference every render flushes the cache. Callers MUST supply a stable reference (module scope or `useMemo`).\n- **`sanitizeSchema`** is tracked by identity AND additionally stabilized internally via a deep-equal safety net (`useStableValue`). An inline-but-deep-equal schema still works, just with a one-time deep compare on each render — cheaper than a cache flush but not free.\n\nWhy the asymmetry: function identity can't be deep-compared (two closures with identical bodies are always non-equal), so for `urlTransform` only the call-site can produce a stable reference. `sanitizeSchema` is plain data, so a deep compare is meaningful and serves as a guardrail for callers who forget the module-scope rule.\n\n```tsx\n// 🚫 Anti-pattern — `urlTransform` is recreated every render and discards\n//    the entire markdown cache. `sanitizeSchema` would too without the\n//    internal deep-equal safety net, but you still pay the deep-compare cost.\n<AIMarkdown\n  urlTransform={(url, k, n) => /* … */}\n  sanitizeSchema={extendSanitizeSchema((s) => /* … */)}\n/>\n\n// ✅ Stable — both refs are minted once at module scope.\nconst URL_TRANSFORM = (url, k, n) => /* … */;\nconst SCHEMA = extendSanitizeSchema((s) => /* … */);\n<AIMarkdown urlTransform={URL_TRANSFORM} sanitizeSchema={SCHEMA} />\n```\n\nIn development the library will `console.warn` after detecting 3+ identity flips on either prop. The warning is dead-code-eliminated in production builds. Define both values at module scope, or memoize with `useMemo` if they depend on state.\n\n### Regex Escaping for `+` / `-` / `.` in Scheme Names\n\nScheme names can contain `+`, `-`, and `.`. Escape `+` and `.` when matching them literally; a hyphen is literal outside a character class. Use `/^web\\+app:/i` for `web+app:`. The unescaped `/^web+app:/i` instead matches `webapp:`, `webbapp:`, and further repetitions of `b`.\n\n### Inspecting the Default Schema\n\n`extendSanitizeSchema` hands the modifier a deep clone of the library default. That makes the helper itself the cleanest introspection path — no separate export of the singleton is needed:\n\n```tsx\nextendSanitizeSchema((s) => {\n  console.log('default sanitize schema:', s);\n});\n```\n\nWhy no direct `sanitizeSchema` export? Because the obvious extension pattern — `{ ...sanitizeSchema, … }` — is a shallow spread. Nested arrays (`protocols.href`, `attributes.a`, `ancestors.*`, …) stay aliased to the singleton; a mutation would target shared nested data. The engine singleton is now deeply frozen, so such writes can throw instead of extending it. A deep clone gives each customization its own mutable arrays. `extendSanitizeSchema` always works on a deep clone, so this class of bug is impossible by construction.\n\n### API Stability of `UrlTransform` and `SanitizeSchema`\n\nBoth prop types track their respective upstream packages — `UrlTransform` follows `react-markdown`'s shape and `SanitizeSchema` follows `rehype-sanitize`'s. They may evolve with those packages' major versions. Hand-construct schemas via the helpers (rather than typing your own from scratch) and you'll inherit any upstream-driven changes automatically.\n\n## Hooks\n\nState is split across **five per-system contexts**. Each narrow hook subscribes to exactly one system and re-renders only when that system changes — a `streaming` flip no longer wakes every consumer. All throw if called outside the provider boundary (except `useAIMarkdownMetadata`, which returns `undefined` when no metadata was provided).\n\n### The five narrow hooks\n\n| Hook                                 | Returns                                                                      |\n| ------------------------------------ | ---------------------------------------------------------------------------- |\n| `useAIMarkdownState()`               | `{ streaming, …extension state groups }`                                     |\n| `useAIMarkdownTheme()`               | `{ fontSize, variant, colorScheme }`                                         |\n| `useAIMarkdownDocument()`            | `{ documentId, documentIdExplicit, clobberPrefix }`                          |\n| `useAIMarkdownBehaviors()`           | `{ blockMemo, incrementalParse, preserveOrphanReferences, …wrapper groups }` |\n| `useAIMarkdownMetadata<TMetadata>()` | `TMetadata \\| undefined`                                                     |\n\n```tsx\nimport { useAIMarkdownState, useAIMarkdownTheme } from '@ai-markdown/react';\n\nfunction CustomCodeBlock({ children }: PropsWithChildren) {\n  const { streaming } = useAIMarkdownState();\n  const { colorScheme } = useAIMarkdownTheme();\n\n  if (streaming) {\n    return <pre className={`streaming ${colorScheme}`}>{children}</pre>;\n  }\n  return <pre className={colorScheme}>{children}</pre>;\n}\n```\n\nField reference:\n\n| Field                                                         | Hook                       | Type                    | Description                                                                                                                                                                                                                                                                                                                                                                          |\n| ------------------------------------------------------------- | -------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `streaming`                                                   | `useAIMarkdownState()`     | `boolean`               | Whether content is being streamed.                                                                                                                                                                                                                                                                                                                                                   |\n| `fontSize`                                                    | `useAIMarkdownTheme()`     | `string`                | Resolved CSS font-size value.                                                                                                                                                                                                                                                                                                                                                        |\n| `variant`                                                     | `useAIMarkdownTheme()`     | `AIMarkdownVariant`     | Active typography variant.                                                                                                                                                                                                                                                                                                                                                           |\n| `colorScheme`                                                 | `useAIMarkdownTheme()`     | `AIMarkdownColorScheme` | Active color scheme.                                                                                                                                                                                                                                                                                                                                                                 |\n| `documentId`                                                  | `useAIMarkdownDocument()`  | `string`                | Stable id for the logical markdown document — caller-supplied or auto-generated via `useId()`.                                                                                                                                                                                                                                                                                       |\n| `documentIdExplicit`                                          | `useAIMarkdownDocument()`  | `boolean`               | Whether `documentId` was explicitly supplied by the caller (vs. auto-generated). Internal coordination signal — `useDocumentRegistry` uses it so an auto-generated id never opts a standalone chunk into cross-chunk coordination. Most custom components can ignore this.                                                                                                           |\n| `clobberPrefix`                                               | `useAIMarkdownDocument()`  | `string`                | URI-safe id prefix derived from `documentId` (with MurmurHash3 → Base62 shortening applied for >16-char ids), used by every clobberable HTML attribute (`id=…` / `href=\"#…\"`). Read this from the hook rather than recomputing locally when writing components that emit anchors — the prefix's exact byte form is not part of the stability contract and may shift across versions. |\n| `blockMemo` / `incrementalParse` / `preserveOrphanReferences` | `useAIMarkdownBehaviors()` | `boolean`               | The resolved behavior switches — same names as the flat props.                                                                                                                                                                                                                                                                                                                       |\n\n### `useAIMarkdown()` — the aggregate\n\n```tsx\nconst { document, metadata, state, theme, behaviors } = useAIMarkdown();\n```\n\nSubscribes to **all five contexts** and re-renders on ANY change — including every `streaming` flip. It serves teaching code and low-frequency components; performance-sensitive components should use the narrow hooks.\n\n### `useAIMarkdownMetadata<TMetadata>()`\n\nRead application data from the metadata context. The hook returns `TMetadata | undefined`; its generic is a compile-time assertion and cannot verify which component supplied the value.\n\n```tsx\nimport { useRef, type PropsWithChildren } from 'react';\nimport { useAIMarkdownMetadata, type AIMarkdownMetadata } from '@ai-markdown/react';\n\ninterface MyMetadata extends AIMarkdownMetadata {\n  onCopyCode: (source: string) => void;\n}\n\nfunction CustomCodeBlock({ children }: PropsWithChildren) {\n  const preRef = useRef<HTMLPreElement>(null);\n  const metadata = useAIMarkdownMetadata<MyMetadata>();\n  return (\n    <div>\n      <button\n        type=\"button\"\n        onClick={() => {\n          metadata?.onCopyCode(preRef.current?.textContent ?? '');\n        }}\n      >\n        Copy\n      </button>\n      <pre ref={preRef}>{children}</pre>\n    </div>\n  );\n}\n```\n\nThis small renderer extracts the visible code text from the actual `<pre>` and keeps the button outside it. `String(children)` would stringify React elements rather than recover their code. For transformed displays, preserve original source from the hast node instead; the [custom component guide](../../docs/custom-components.md) provides that fuller recipe.\n\nMetadata is passed through without a deep-equality wrapper. Reuse a stable object when values have not changed; use a new object when reactive metadata changes. A stable container holding callbacks or an external store is useful for high-frequency application data, but mutating a ref alone does not notify a React view.\n\n### `useStableValue<T>(value: T)`\n\nReturns a referentially stable version of `value`. On each render the new value is deep-compared (via `lodash/isEqual`) against the previous one. If they are structurally equal, the previous reference is returned, preventing unnecessary re-renders in downstream `useMemo`/`useEffect` consumers.\n\n```tsx\nimport { useStableValue } from '@ai-markdown/react';\n\nconst stableConfig = useStableValue(config);\n// stableConfig keeps the same reference as long as config is deep-equal.\n```\n\n### `useStableRecord(record, table)`\n\nThe stability firewall used internally, exported for wrapper authors. Returns a referentially stable version of `record` according to a per-key `AIMarkdownStabilityPolicy` table:\n\n- `DEEP_EQUAL` — restore the previous reference when the new value is deep-equal (plain-data props).\n- `WARN_ONLY` — pass through, but warn in dev after repeated identity flips (functions/components, where deep comparison is meaningless).\n- `PASS_THROUGH` — declared exemption, no stabilization (e.g. `metadata`).\n\nA wrapper builds a table only for the object props it terminates itself (e.g. mantine's `codeBlock`); props forwarded to `<AIMarkdown>` ride the React adapter's firewall untouched.\n\n```tsx\nimport { useStableRecord, AIMarkdownStabilityPolicy, type AIMarkdownStabilityTable } from '@ai-markdown/react';\n\nconst TABLE: AIMarkdownStabilityTable<{ panel: Partial<PanelOptions> | undefined }> = {\n  panel: AIMarkdownStabilityPolicy.DEEP_EQUAL,\n};\n\nconst stable = useStableRecord({ panel }, TABLE);\n```\n\n## Additive Providers\n\nThe React adapter exports two stackable Providers — `AIMarkdownBehaviorsProvider` and `AIMarkdownStateProvider` — so wrappers and applications can transport their own extension groups through the React adapter's contexts. Stack the Provider **outside** `<AIMarkdown>`; consumers still see exactly one context:\n\n```tsx\nimport { useMemo } from 'react';\nimport AIMarkdown, { AIMarkdownBehaviorsProvider, type AIMarkdownBehaviorGroups } from '@ai-markdown/react';\n\nconst NO_GROUPS: AIMarkdownBehaviorGroups = Object.freeze({});\n\nfunction MyMarkdown({ panel, ...rest }: MyMarkdownProps) {\n  // Absent prop → contribute NO group (an outer app-level Provider's\n  // `panel` group then stays visible); present prop wins via inner-wins.\n  const groups = useMemo<AIMarkdownBehaviorGroups>(() => (panel != null ? { panel } : NO_GROUPS), [panel]);\n  return (\n    <AIMarkdownBehaviorsProvider value={groups}>\n      <AIMarkdown {...rest} />\n    </AIMarkdownBehaviorsProvider>\n  );\n}\n```\n\n- **Built-in React prop keys are locked.** Behaviors (`blockMemo`, `incrementalParse`, `preserveOrphanReferences`) and state (`streaming`) cannot be injected from outside — type-forbidden, unconditionally overwritten by the prop-resolved values at the innermost merge, and warned about in dev.\n- Multi-level wrappers stack naturally; for a duplicated group key the inner layer wins.\n- `AIMarkdownStateProvider` carries extension lifecycle states (aborted, reasoning, tool-call-in-progress, …). Group members must be message-lifecycle frequency — frame-rate data (per-token progress etc.) still goes through metadata's stable-container pattern.\n- Apply group defaults inside your wrapper's narrow hook exactly once (the pattern behind mantine's `useMantineCodeBlockOptions()`); bare `??` fallbacks at multiple read sites will drift.\n\n### Group-key registry\n\nGroup keys share one namespace per context (behaviors, state) across every wrapper layer and the application — a duplicated key resolves by inner-wins **silently**, so this re","readmeFilename":""}