[{"data":1,"prerenderedAt":1245},["ShallowReactive",2],{"navigation":3,"$fMW9jOOkNpZriWVK3P_bZYNOyYR2IEPvrHsC7cDBlJJc":446,"/docs/components/message-scroller":459,"surround-/docs/components/message-scroller":1242},[4],{"title":5,"path":6,"stem":7,"children":8,"page":18},"Docs","/docs","docs",[9,294,320,338,351,377,390],{"title":10,"path":11,"stem":12,"children":13,"new":18,"type":293},"Components","/docs/components","docs/02.components",[14,20,24,28,32,37,41,45,49,53,57,61,65,69,73,77,81,85,89,93,97,101,105,109,113,117,121,125,129,133,137,141,145,149,153,157,161,165,169,173,177,181,185,189,193,197,201,205,209,213,217,221,225,229,233,237,241,245,249,253,257,261,265,269,273,277,281,285,289],{"title":15,"path":16,"stem":17,"new":18,"type":19,"children":-1},"Accordion","/docs/components/accordion","docs/components/accordion",false,"component",{"title":21,"path":22,"stem":23,"new":18,"type":19,"children":-1},"Alert","/docs/components/alert","docs/components/alert",{"title":25,"path":26,"stem":27,"new":18,"type":19,"children":-1},"Alert Dialog","/docs/components/alert-dialog","docs/components/alert-dialog",{"title":29,"path":30,"stem":31,"new":18,"type":19,"children":-1},"Aspect Ratio","/docs/components/aspect-ratio","docs/components/aspect-ratio",{"title":33,"path":34,"stem":35,"new":36,"type":19,"children":-1},"Attachment","/docs/components/attachment","docs/components/attachment",true,{"title":38,"path":39,"stem":40,"new":18,"type":19,"children":-1},"Avatar","/docs/components/avatar","docs/components/avatar",{"title":42,"path":43,"stem":44,"new":18,"type":19,"children":-1},"Badge","/docs/components/badge","docs/components/badge",{"title":46,"path":47,"stem":48,"new":18,"type":19,"children":-1},"Breadcrumb","/docs/components/breadcrumb","docs/components/breadcrumb",{"title":50,"path":51,"stem":52,"new":36,"type":19,"children":-1},"Bubble","/docs/components/bubble","docs/components/bubble",{"title":54,"path":55,"stem":56,"new":18,"type":19,"children":-1},"Button","/docs/components/button","docs/components/button",{"title":58,"path":59,"stem":60,"new":18,"type":19,"children":-1},"Button Group","/docs/components/button-group","docs/components/button-group",{"title":62,"path":63,"stem":64,"new":18,"type":19,"children":-1},"Calendar","/docs/components/calendar","docs/components/calendar",{"title":66,"path":67,"stem":68,"new":18,"type":19,"children":-1},"Card","/docs/components/card","docs/components/card",{"title":70,"path":71,"stem":72,"new":18,"type":19,"children":-1},"Carousel","/docs/components/carousel","docs/components/carousel",{"title":74,"path":75,"stem":76,"new":18,"type":19,"children":-1},"Chart","/docs/components/chart","docs/components/chart",{"title":78,"path":79,"stem":80,"new":18,"type":19,"children":-1},"Checkbox","/docs/components/checkbox","docs/components/checkbox",{"title":82,"path":83,"stem":84,"new":18,"type":19,"children":-1},"Collapsible","/docs/components/collapsible","docs/components/collapsible",{"title":86,"path":87,"stem":88,"new":18,"type":19,"children":-1},"Combobox","/docs/components/combobox","docs/components/combobox",{"title":90,"path":91,"stem":92,"new":18,"type":19,"children":-1},"Command","/docs/components/command","docs/components/command",{"title":94,"path":95,"stem":96,"new":18,"type":19,"children":-1},"Context Menu","/docs/components/context-menu","docs/components/context-menu",{"title":98,"path":99,"stem":100,"new":18,"type":19,"children":-1},"Data Table","/docs/components/data-table","docs/components/data-table",{"title":102,"path":103,"stem":104,"new":18,"type":19,"children":-1},"Date Picker","/docs/components/date-picker","docs/components/date-picker",{"title":106,"path":107,"stem":108,"new":18,"type":19,"children":-1},"Dialog","/docs/components/dialog","docs/components/dialog",{"title":110,"path":111,"stem":112,"new":18,"type":19,"children":-1},"Drawer","/docs/components/drawer","docs/components/drawer",{"title":114,"path":115,"stem":116,"new":18,"type":19,"children":-1},"Dropdown Menu","/docs/components/dropdown-menu","docs/components/dropdown-menu",{"title":118,"path":119,"stem":120,"new":18,"type":19,"children":-1},"Empty","/docs/components/empty","docs/components/empty",{"title":122,"path":123,"stem":124,"new":18,"type":19,"children":-1},"Field","/docs/components/field","docs/components/field",{"title":126,"path":127,"stem":128,"new":18,"type":19,"children":-1},"Form","/docs/components/form","docs/components/form",{"title":130,"path":131,"stem":132,"new":18,"type":19,"children":-1},"Hover Card","/docs/components/hover-card","docs/components/hover-card",{"title":134,"path":135,"stem":136,"new":18,"type":19,"children":-1},"Input","/docs/components/input","docs/components/input",{"title":138,"path":139,"stem":140,"new":18,"type":19,"children":-1},"Input Group","/docs/components/input-group","docs/components/input-group",{"title":142,"path":143,"stem":144,"new":18,"type":19,"children":-1},"Input OTP","/docs/components/input-otp","docs/components/input-otp",{"title":146,"path":147,"stem":148,"new":18,"type":19,"children":-1},"Item","/docs/components/item","docs/components/item",{"title":150,"path":151,"stem":152,"new":18,"type":19,"children":-1},"Kbd","/docs/components/kbd","docs/components/kbd",{"title":154,"path":155,"stem":156,"new":18,"type":19,"children":-1},"Label","/docs/components/label","docs/components/label",{"title":158,"path":159,"stem":160,"new":36,"type":19,"children":-1},"Marker","/docs/components/marker","docs/components/marker",{"title":162,"path":163,"stem":164,"new":18,"type":19,"children":-1},"Menubar","/docs/components/menubar","docs/components/menubar",{"title":166,"path":167,"stem":168,"new":36,"type":19,"children":-1},"Message","/docs/components/message","docs/components/message",{"title":170,"path":171,"stem":172,"new":36,"type":19,"children":-1},"Message Scroller","/docs/components/message-scroller","docs/components/message-scroller",{"title":174,"path":175,"stem":176,"new":18,"type":19,"children":-1},"Native Select","/docs/components/native-select","docs/components/native-select",{"title":178,"path":179,"stem":180,"new":18,"type":19,"children":-1},"Navigation Menu","/docs/components/navigation-menu","docs/components/navigation-menu",{"title":182,"path":183,"stem":184,"new":18,"type":19,"children":-1},"Number Field","/docs/components/number-field","docs/components/number-field",{"title":186,"path":187,"stem":188,"new":18,"type":19,"children":-1},"Pagination","/docs/components/pagination","docs/components/pagination",{"title":190,"path":191,"stem":192,"new":18,"type":19,"children":-1},"Pin Input","/docs/components/pin-input","docs/components/pin-input",{"title":194,"path":195,"stem":196,"new":18,"type":19,"children":-1},"Popover","/docs/components/popover","docs/components/popover",{"title":198,"path":199,"stem":200,"new":18,"type":19,"children":-1},"Progress","/docs/components/progress","docs/components/progress",{"title":202,"path":203,"stem":204,"new":18,"type":19,"children":-1},"Radio Group","/docs/components/radio-group","docs/components/radio-group",{"title":206,"path":207,"stem":208,"new":18,"type":19,"children":-1},"Range Calendar","/docs/components/range-calendar","docs/components/range-calendar",{"title":210,"path":211,"stem":212,"new":18,"type":19,"children":-1},"Resizable","/docs/components/resizable","docs/components/resizable",{"title":214,"path":215,"stem":216,"new":18,"type":19,"children":-1},"Scroll Area","/docs/components/scroll-area","docs/components/scroll-area",{"title":218,"path":219,"stem":220,"new":18,"type":19,"children":-1},"Select","/docs/components/select","docs/components/select",{"title":222,"path":223,"stem":224,"new":18,"type":19,"children":-1},"Separator","/docs/components/separator","docs/components/separator",{"title":226,"path":227,"stem":228,"new":18,"type":19,"children":-1},"Sheet","/docs/components/sheet","docs/components/sheet",{"title":230,"path":231,"stem":232,"new":18,"type":19,"children":-1},"Sidebar","/docs/components/sidebar","docs/components/sidebar",{"title":234,"path":235,"stem":236,"new":18,"type":19,"children":-1},"Skeleton","/docs/components/skeleton","docs/components/skeleton",{"title":238,"path":239,"stem":240,"new":18,"type":19,"children":-1},"Slider","/docs/components/slider","docs/components/slider",{"title":242,"path":243,"stem":244,"new":18,"type":19,"children":-1},"Sonner","/docs/components/sonner","docs/components/sonner",{"title":246,"path":247,"stem":248,"new":18,"type":19,"children":-1},"Spinner","/docs/components/spinner","docs/components/spinner",{"title":250,"path":251,"stem":252,"new":18,"type":19,"children":-1},"Stepper","/docs/components/stepper","docs/components/stepper",{"title":254,"path":255,"stem":256,"new":18,"type":19,"children":-1},"Switch","/docs/components/switch","docs/components/switch",{"title":258,"path":259,"stem":260,"new":18,"type":19,"children":-1},"Table","/docs/components/table","docs/components/table",{"title":262,"path":263,"stem":264,"new":18,"type":19,"children":-1},"Tabs","/docs/components/tabs","docs/components/tabs",{"title":266,"path":267,"stem":268,"new":18,"type":19,"children":-1},"Tags Input","/docs/components/tags-input","docs/components/tags-input",{"title":270,"path":271,"stem":272,"new":18,"type":19,"children":-1},"Textarea","/docs/components/textarea","docs/components/textarea",{"title":274,"path":275,"stem":276,"new":18,"type":19,"children":-1},"Toast","/docs/components/toast","docs/components/toast",{"title":278,"path":279,"stem":280,"new":18,"type":19,"children":-1},"Toggle","/docs/components/toggle","docs/components/toggle",{"title":282,"path":283,"stem":284,"new":18,"type":19,"children":-1},"Toggle Group","/docs/components/toggle-group","docs/components/toggle-group",{"title":286,"path":287,"stem":288,"new":18,"type":19,"children":-1},"Tooltip","/docs/components/tooltip","docs/components/tooltip",{"title":290,"path":291,"stem":292,"new":18,"type":19,"children":-1},"Typography","/docs/components/typography","docs/components/typography","group",{"title":295,"path":296,"stem":297,"children":298,"new":18,"type":293},"Installation","/docs/installation","docs/02.installation",[299,304,308,312,316],{"title":300,"path":301,"stem":302,"new":18,"type":303,"children":-1},"Vite","/docs/installation/vite","docs/installation/01.vite","page",{"title":305,"path":306,"stem":307,"new":18,"type":303,"children":-1},"Nuxt","/docs/installation/nuxt","docs/installation/02.nuxt",{"title":309,"path":310,"stem":311,"new":18,"type":303,"children":-1},"Astro","/docs/installation/astro","docs/installation/03.astro",{"title":313,"path":314,"stem":315,"new":18,"type":303,"children":-1},"Laravel","/docs/installation/laravel","docs/installation/04.laravel",{"title":317,"path":318,"stem":319,"new":18,"type":303,"children":-1},"Manual Installation","/docs/installation/manual","docs/installation/05.manual",{"title":321,"path":322,"stem":323,"children":324,"new":18,"type":293},"Dark Mode","/docs/dark-mode","docs/05.dark-mode",[325,328,331,335],{"title":300,"path":326,"stem":327,"new":18,"type":303,"children":-1},"/docs/dark-mode/vite","docs/dark-mode/01.vite",{"title":305,"path":329,"stem":330,"new":18,"type":303,"children":-1},"/docs/dark-mode/nuxt","docs/dark-mode/02.nuxt",{"title":332,"path":333,"stem":334,"new":18,"type":303,"children":-1},"Vitepress","/docs/dark-mode/vitepress","docs/dark-mode/03.vitepress",{"title":309,"path":336,"stem":337,"new":18,"type":303,"children":-1},"/docs/dark-mode/astro","docs/dark-mode/04.astro",{"title":339,"path":340,"stem":341,"children":342,"new":18,"type":293},"Forms","/docs/forms","docs/forms",[343,347],{"title":344,"path":345,"stem":346,"new":18,"type":303,"children":-1},"VeeValidate","/docs/forms/vee-validate","docs/forms/01.vee-validate",{"title":348,"path":349,"stem":350,"new":18,"type":303,"children":-1},"TanStack Form","/docs/forms/tanstack-form","docs/forms/02.tanstack-form",{"title":352,"path":353,"stem":354,"children":355,"new":18,"type":293},"Registry","/docs/registry","docs/registry/index",[356,357,361,365,369,373],{"title":352,"path":353,"stem":354,"new":18,"type":303,"children":-1},{"title":358,"path":359,"stem":360,"new":18,"type":303,"children":-1},"Examples","/docs/registry/examples","docs/registry/examples",{"title":362,"path":363,"stem":364,"new":18,"type":303,"children":-1},"FAQ","/docs/registry/faq","docs/registry/faq",{"title":366,"path":367,"stem":368,"new":18,"type":303,"children":-1},"Getting Started","/docs/registry/getting-started","docs/registry/getting-started",{"title":370,"path":371,"stem":372,"new":18,"type":303,"children":-1},"registry-item.json","/docs/registry/registry-item-json","docs/registry/registry-item-json",{"title":374,"path":375,"stem":376,"new":18,"type":303,"children":-1},"registry.json","/docs/registry/registry-json","docs/registry/registry-json",{"title":378,"path":379,"stem":380,"children":381,"page":18,"type":293},"Utilities","/docs/utilities","docs/utilities",[382,386],{"title":383,"path":384,"stem":385,"new":18,"type":303,"children":-1},"scroll-fade","/docs/utilities/scroll-fade","docs/utilities/scroll-fade",{"title":387,"path":388,"stem":389,"new":18,"type":303,"children":-1},"shimmer","/docs/utilities/shimmer","docs/utilities/shimmer",{"path":6,"stem":7,"title":391,"type":293,"children":392},"Get Started",[393,397,404,408,412,418,422,426,430,434,438,442],{"title":394,"path":395,"stem":396,"new":18,"type":303,"children":-1},"Introduction","/docs/introduction","docs/01.introduction",{"title":295,"path":296,"stem":297,"children":398,"new":18,"type":293},[399,400,401,402,403],{"title":300,"path":301,"stem":302,"new":18,"type":303,"children":-1},{"title":305,"path":306,"stem":307,"new":18,"type":303,"children":-1},{"title":309,"path":310,"stem":311,"new":18,"type":303,"children":-1},{"title":313,"path":314,"stem":315,"new":18,"type":303,"children":-1},{"title":317,"path":318,"stem":319,"new":18,"type":303,"children":-1},{"title":405,"path":406,"stem":407,"new":18,"type":303,"children":-1},"components.json","/docs/components-json","docs/03.components-json",{"title":409,"path":410,"stem":411,"new":18,"type":303,"children":-1},"Theming","/docs/theming","docs/04.theming",{"title":321,"path":322,"stem":323,"children":413,"new":18,"type":293},[414,415,416,417],{"title":300,"path":326,"stem":327,"new":18,"type":303,"children":-1},{"title":305,"path":329,"stem":330,"new":18,"type":303,"children":-1},{"title":332,"path":333,"stem":334,"new":18,"type":303,"children":-1},{"title":309,"path":336,"stem":337,"new":18,"type":303,"children":-1},{"title":419,"path":420,"stem":421,"new":18,"type":303,"children":-1},"CLI","/docs/cli","docs/06.cli",{"title":423,"path":424,"stem":425,"new":18,"type":303,"children":-1},"JavaScript","/docs/javascript","docs/07.javascript",{"title":427,"path":428,"stem":429,"new":18,"type":303,"children":-1},"Figma","/docs/figma","docs/09.figma",{"title":431,"path":432,"stem":433,"new":18,"type":303,"children":-1},"Changelog","/docs/changelog","docs/10.changelog",{"title":435,"path":436,"stem":437,"new":18,"type":303,"children":-1},"Legacy Docs","/docs/legacy","docs/11.legacy",{"title":439,"path":440,"stem":441,"new":18,"type":303,"children":-1},"Skills","/docs/skills","docs/skills",{"title":378,"path":379,"stem":380,"children":443,"page":18,"type":293},[444,445],{"title":383,"path":384,"stem":385,"new":18,"type":303,"children":-1},{"title":387,"path":388,"stem":389,"new":18,"type":303,"children":-1},{"repo":447},{"id":448,"name":449,"repo":450,"description":451,"createdAt":452,"updatedAt":453,"pushedAt":454,"stars":455,"watchers":456,"forks":457,"defaultBranch":458},658791894,"shadcn-vue","unovue/shadcn-vue","Vue port of shadcn-ui","2023-06-26T13:53:23Z","2026-07-10T15:52:04Z","2026-07-10T15:50:34Z",10260,37,665,"dev",{"id":460,"title":170,"body":461,"description":1235,"extension":1236,"links":1237,"meta":1238,"navigation":36,"new":36,"path":171,"rawbody":1239,"seo":1240,"stem":172,"__hash__":1241},"content/docs/components/message-scroller.md",{"type":462,"value":463,"toc":1208},"minimark",[464,471,476,484,492,495,550,554,563,566,570,578,582,587,599,605,608,612,616,619,623,627,634,638,642,652,656,660,667,671,675,686,690,694,704,708,712,723,729,733,737,752,756,760,766,770,774,777,788,910,913,947,962,966,1016,1020,1114,1129,1133,1138,1146,1175,1179,1185,1191,1195,1201],[465,466],"component-preview",{"className":467,"name":469,"previewClass":470},[468],"style-luma","MessageScrollerDemo","theme-green bg-surface dark:bg-background",[472,473,475],"h2",{"id":474},"messagescroller","MessageScroller",[477,478,479,480,483],"p",{},"A great streaming chat scroller has to juggle a lot at once: pin to the live edge while a reply streams, but never fight a reader who scrolls up; anchor each new turn near the top with a peek of the previous exchange; preserve position when older history loads above; and expose commands to jump anywhere in the thread. ",[481,482,475],"code",{}," owns those hard parts so your message list doesn't have to.",[477,485,486,487,491],{},"It does ",[488,489,490],"strong",{},"not"," own your messages, AI state, transport, or model — it is a headless scroll container you compose around your own rows.",[472,493,295],{"id":494},"installation",[496,497,498,513,526],"code-tabs",{},[499,500,501,507],"tabs-list",{},[502,503,505],"tabs-trigger",{"value":504},"cli",[477,506,419],{},[502,508,510],{"value":509},"manual",[477,511,512],{},"Manual",[514,515,516],"tabs-content",{"value":504},[517,518,524],"pre",{"className":519,"code":521,"language":522,"meta":523},[520],"language-bash","npx shadcn-vue@latest add message-scroller\n","bash","",[481,525,521],{"__ignoreMap":523},[514,527,528],{"value":509},[529,530,531,545],"steps",{},[532,533,534],"step",{},[477,535,536,537,544],{},"Copy and paste the ",[538,539,543],"a",{"href":540,"rel":541},"https://github.com/unovue/shadcn-vue/tree/dev/apps/v4/registry/bases/reka/ui/message-scroller",[542],"nofollow","GitHub source code"," into your project.",[532,546,547],{},[477,548,549],{},"Update the import paths to match your project setup.",[472,551,553],{"id":552},"usage","Usage",[517,555,561],{"className":556,"code":558,"language":559,"meta":560},[557],"language-vue","\u003Cscript setup lang=\"ts\">\nimport {\n  MessageScroller,\n  MessageScrollerButton,\n  MessageScrollerContent,\n  MessageScrollerItem,\n  MessageScrollerProvider,\n  MessageScrollerViewport,\n} from '@/components/ui/message-scroller'\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CMessageScrollerProvider auto-scroll default-scroll-position=\"last-anchor\">\n    \u003CMessageScroller>\n      \u003CMessageScrollerViewport>\n        \u003CMessageScrollerContent>\n          \u003CMessageScrollerItem\n            v-for=\"message in messages\"\n            :key=\"message.id\"\n            :message-id=\"message.id\"\n            :scroll-anchor=\"message.role === 'user'\"\n          >\n            \u003C!-- Message / Bubble / Marker goes here -->\n          \u003C/MessageScrollerItem>\n        \u003C/MessageScrollerContent>\n      \u003C/MessageScrollerViewport>\n      \u003CMessageScrollerButton direction=\"end\" />\n    \u003C/MessageScroller>\n  \u003C/MessageScrollerProvider>\n\u003C/template>\n","vue","showLineNumbers",[481,562,558],{"__ignoreMap":523},[477,564,565],{},"The provider must have a constrained height (or a height-bounded parent) so the viewport can scroll.",[472,567,569],{"id":568},"composition","Composition",[517,571,576],{"className":572,"code":574,"language":575,"meta":523},[573],"language-text","MessageScrollerProvider\n└── MessageScroller\n    ├── MessageScrollerViewport\n    │   └── MessageScrollerContent\n    │       └── MessageScrollerItem\n    └── MessageScrollerButton\n","text",[481,577,574],{"__ignoreMap":523},[472,579,581],{"id":580},"core-concepts","Core Concepts",[583,584,586],"h3",{"id":585},"anchoring-turns","Anchoring Turns",[477,588,589,590,594,595,598],{},"A turn is the part of the conversation that starts a new exchange — usually the user's message and the assistant reply that follows. An ",[591,592,593],"em",{},"anchor"," is the row the viewport should treat as the start of that turn. Mark that row with ",[481,596,597],{},"scrollAnchor",". When a new anchor is appended, the viewport moves it near the top and keeps a peek of the previous item above it, so the new turn does not feel detached from its context.",[517,600,603],{"className":601,"code":602,"language":559,"meta":523},[557],"\u003CMessageScrollerItem\n  :message-id=\"message.id\"\n  :scroll-anchor=\"message.role === 'user'\"\n>\n  \u003C!-- ... -->\n\u003C/MessageScrollerItem>\n",[481,604,602],{"__ignoreMap":523},[477,606,607],{},"Scroll anchors are not tied to message role. You can turn any row into an anchor: a user message, a system marker, a handoff event, or anything else that starts a meaningful turn.",[465,609],{"className":610,"name":611,"previewClass":470},[468],"MessageScrollerAnchoringDemo",[583,613,615],{"id":614},"group-chat","Group Chat",[477,617,618],{},"In a group chat, the turn boundary is often the message that asks the model to respond, or a marker like \"Marcus joined the chat\". Typing indicators and history controls usually should not anchor. Because anchoring is role-independent, you can anchor a marker just as easily as a message.",[465,620],{"className":621,"name":622,"previewClass":470},[468],"MessageScrollerGroupChatDemo",[583,624,626],{"id":625},"keeping-context-visible","Keeping Context Visible",[477,628,629,630,633],{},"When a new turn starts, it should still feel like part of the same continuous thread. ",[481,631,632],{},"scrollPreviousItemPeek"," keeps a slice of the previous item visible above the anchor, so the reader keeps their context instead of feeling like the conversation restarted on a blank page.",[465,635],{"className":636,"name":637,"previewClass":470},[468],"MessageScrollerPreviousContextDemo",[583,639,641],{"id":640},"following-the-live-edge","Following the Live Edge",[477,643,644,645,648,649,651],{},"When the reader is at the live edge, ",[481,646,647],{},"autoScroll"," keeps streamed replies in view as they grow. Scrolling away from the live edge — by wheel, touch, keyboard, or dragging the scrollbar — releases the view, so new chunks arrive without moving the reader. ",[481,650,647],{}," composes with turn anchoring: when a new turn anchors near the top, the view stays put while the reply streams into the room below it.",[465,653],{"className":654,"name":655,"previewClass":470},[468],"MessageScrollerStreamingDemo",[583,657,659],{"id":658},"opening-saved-threads","Opening Saved Threads",[477,661,662,663,666],{},"Reopening a saved thread at the absolute end often drops the reader in without enough context. A better default is ",[481,664,665],{},"\"last-anchor\"",": show the last meaningful turn, like the user's latest message, with the reply below it.",[465,668],{"className":669,"name":670,"previewClass":470},[468],"MessageScrollerOpeningPositionDemo",[583,672,674],{"id":673},"loading-earlier-messages","Loading Earlier Messages",[477,676,677,678,681,682,685],{},"Loading earlier messages should not move the conversation the reader is already looking at. When older rows are prepended above the current transcript, ",[481,679,680],{},"MessageScrollerViewport"," preserves the visible row so the reader stays in the same place while history loads above them. This is enabled by default through ",[481,683,684],{},"preserveScrollOnPrepend",".",[465,687],{"className":688,"name":689,"previewClass":470},[468],"MessageScrollerLoadHistoryDemo",[583,691,693],{"id":692},"animating-new-messages","Animating New Messages",[477,695,696,697,700,701,703],{},"A common chat pattern is to animate the user's message when it is sent, then let the assistant reply stream into a regular row below it. Keep ",[481,698,699],{},"messageId"," and ",[481,702,597],{}," on the animated item and use transform and opacity for the entrance — avoid animating height, margin, or padding, which can fight the scroller's positioning.",[465,705],{"className":706,"name":707,"previewClass":470},[468],"MessageScrollerAnimationDemo",[583,709,711],{"id":710},"jumping-to-messages","Jumping to Messages",[477,713,714,715,718,719,722],{},"Search results, permalinks, outline items, and toolbar buttons often need to drive the transcript from outside the message list. Use ",[481,716,717],{},"useMessageScroller"," for those controls — the composables read from ",[481,720,721],{},"MessageScrollerProvider",", so they work in any component inside the provider.",[517,724,727],{"className":725,"code":726,"language":559,"meta":523},[557],"\u003Cscript setup lang=\"ts\">\nimport { useMessageScroller } from '@/components/ui/message-scroller'\n\nconst { scrollToMessage, scrollToEnd, scrollToStart } = useMessageScroller()\n\u003C/script>\n",[481,728,726],{"__ignoreMap":523},[465,730],{"className":731,"name":732,"previewClass":470},[468],"MessageScrollerCommandsDemo",[583,734,736],{"id":735},"tracking-the-readers-position","Tracking the Reader's Position",[477,738,739,740,743,744,747,748,751],{},"Use ",[481,741,742],{},"useMessageScrollerVisibility"," to track the reader's position — a table-of-contents or jump menu that highlights the current anchored turn. ",[481,745,746],{},"currentAnchorId"," answers \"where am I\" and stays set after that anchor scrolls above the viewport; ",[481,749,750],{},"visibleMessageIds"," answers \"what is on screen\", in document order.",[465,753],{"className":754,"name":755,"previewClass":470},[468],"MessageScrollerVisibilityDemo",[583,757,759],{"id":758},"reading-scroll-state","Reading Scroll State",[477,761,739,762,765],{},[481,763,764],{},"useMessageScrollerScrollable"," when you need scroll state in JavaScript, such as a status indicator or a custom \"jump to latest\" control. It reports which edges the viewport can still scroll toward.",[465,767],{"className":768,"name":769,"previewClass":470},[468],"MessageScrollerScrollableDemo",[472,771,773],{"id":772},"api-reference","API Reference",[583,775,721],{"id":776},"messagescrollerprovider",[477,778,779,780,783,784,787],{},"Owns the scroll state and behavior. Provide it via ",[481,781,782],{},"provide","/",[481,785,786],{},"inject"," and expose the scroll composables to descendants.",[789,790,791,810],"table",{},[792,793,794],"thead",{},[795,796,797,801,804,807],"tr",{},[798,799,800],"th",{},"Prop",[798,802,803],{},"Type",[798,805,806],{},"Default",[798,808,809],{},"Description",[811,812,813,833,853,873,891],"tbody",{},[795,814,815,820,825,830],{},[816,817,818],"td",{},[481,819,647],{},[816,821,822],{},[481,823,824],{},"boolean",[816,826,827],{},[481,828,829],{},"false",[816,831,832],{},"Follow the live edge while the reader is pinned to the bottom.",[795,834,835,840,845,850],{},[816,836,837],{},[481,838,839],{},"defaultScrollPosition",[816,841,842],{},[481,843,844],{},"'start' | 'end' | 'last-anchor'",[816,846,847],{},[481,848,849],{},"'end'",[816,851,852],{},"Opening position for the transcript.",[795,854,855,860,865,870],{},[816,856,857],{},[481,858,859],{},"scrollEdgeThreshold",[816,861,862],{},[481,863,864],{},"number",[816,866,867],{},[481,868,869],{},"8",[816,871,872],{},"Distance in px from an edge before it is considered scrollable.",[795,874,875,879,883,888],{},[816,876,877],{},[481,878,632],{},[816,880,881],{},[481,882,864],{},[816,884,885],{},[481,886,887],{},"64",[816,889,890],{},"Amount in px of the previous turn kept visible when anchoring.",[795,892,893,898,902,907],{},[816,894,895],{},[481,896,897],{},"scrollMargin",[816,899,900],{},[481,901,864],{},[816,903,904],{},[481,905,906],{},"0",[816,908,909],{},"Extra offset in px applied when scrolling to an element.",[583,911,680],{"id":912},"messagescrollerviewport",[789,914,915,927],{},[792,916,917],{},[795,918,919,921,923,925],{},[798,920,800],{},[798,922,803],{},[798,924,806],{},[798,926,809],{},[811,928,929],{},[795,930,931,935,939,944],{},[816,932,933],{},[481,934,684],{},[816,936,937],{},[481,938,824],{},[816,940,941],{},[481,942,943],{},"true",[816,945,946],{},"Keep the current view when messages are added above.",[477,948,949,950,953,954,957,958,961],{},"Rendered as a ",[481,951,952],{},"role=\"region\"",", ",[481,955,956],{},"aria-label=\"Messages\"",", focusable (",[481,959,960],{},"tabindex=\"0\"",") native scroll container.",[583,963,965],{"id":964},"messagescrolleritem","MessageScrollerItem",[789,967,968,980],{},[792,969,970],{},[795,971,972,974,976,978],{},[798,973,800],{},[798,975,803],{},[798,977,806],{},[798,979,809],{},[811,981,982,999],{},[795,983,984,988,993,996],{},[816,985,986],{},[481,987,699],{},[816,989,990],{},[481,991,992],{},"string",[816,994,995],{},"—",[816,997,998],{},"Stable id used for anchoring, visibility, jumps.",[795,1000,1001,1005,1009,1013],{},[816,1002,1003],{},[481,1004,597],{},[816,1006,1007],{},[481,1008,824],{},[816,1010,1011],{},[481,1012,829],{},[816,1014,1015],{},"Marks this row as the start of a turn.",[583,1017,1019],{"id":1018},"messagescrollerbutton","MessageScrollerButton",[789,1021,1022,1034],{},[792,1023,1024],{},[795,1025,1026,1028,1030,1032],{},[798,1027,800],{},[798,1029,803],{},[798,1031,806],{},[798,1033,809],{},[811,1035,1036,1055,1075,1095],{},[795,1037,1038,1043,1048,1052],{},[816,1039,1040],{},[481,1041,1042],{},"direction",[816,1044,1045],{},[481,1046,1047],{},"'start' | 'end'",[816,1049,1050],{},[481,1051,849],{},[816,1053,1054],{},"Direction the button scrolls toward.",[795,1056,1057,1062,1067,1072],{},[816,1058,1059],{},[481,1060,1061],{},"behavior",[816,1063,1064],{},[481,1065,1066],{},"ScrollBehavior",[816,1068,1069],{},[481,1070,1071],{},"'smooth'",[816,1073,1074],{},"Scroll behavior for the jump.",[795,1076,1077,1082,1087,1092],{},[816,1078,1079],{},[481,1080,1081],{},"variant",[816,1083,1084],{},[481,1085,1086],{},"ButtonVariants",[816,1088,1089],{},[481,1090,1091],{},"'secondary'",[816,1093,1094],{},"Button variant.",[795,1096,1097,1102,1106,1111],{},[816,1098,1099],{},[481,1100,1101],{},"size",[816,1103,1104],{},[481,1105,1086],{},[816,1107,1108],{},[481,1109,1110],{},"'icon-sm'",[816,1112,1113],{},"Button size.",[477,1115,1116,1117,1120,1121,1124,1125,1128],{},"Exposes ",[481,1118,1119],{},"data-active"," for styling and becomes ",[481,1122,1123],{},"inert"," with ",[481,1126,1127],{},"tabindex=\"-1\""," when there is nothing to scroll toward.",[583,1130,1132],{"id":1131},"composables","Composables",[1134,1135,1137],"h4",{"id":1136},"usemessagescroller","useMessageScroller()",[517,1139,1144],{"className":1140,"code":1142,"language":1143,"meta":523},[1141],"language-ts","const { scrollToMessage, scrollToEnd, scrollToStart } = useMessageScroller()\n","ts",[481,1145,1142],{"__ignoreMap":523},[1147,1148,1149,1165],"ul",{},[1150,1151,1152,1155,1156,1158,1159,1161,1162,1164],"li",{},[481,1153,1154],{},"scrollToMessage(id, options?)"," — scroll to the item with the matching ",[481,1157,699],{},". Returns ",[481,1160,943],{}," if handled (queued if the item is not mounted yet), ",[481,1163,829],{}," if the id is missing after rows have mounted.",[1150,1166,1167,1170,1171,1174],{},[481,1168,1169],{},"scrollToEnd(options?)"," / ",[481,1172,1173],{},"scrollToStart(options?)"," — scroll to the live edge or the top.",[1134,1176,1178],{"id":1177},"usemessagescrollervisibility","useMessageScrollerVisibility()",[517,1180,1183],{"className":1181,"code":1182,"language":1143,"meta":523},[1141],"const visibility = useMessageScrollerVisibility()\n// visibility.value.currentAnchorId, visibility.value.visibleMessageIds\n",[481,1184,1182],{"__ignoreMap":523},[477,1186,1187,1188,1190],{},"Tracking only runs while something subscribes, and rows need a ",[481,1189,699],{}," to participate.",[1134,1192,1194],{"id":1193},"usemessagescrollerscrollable","useMessageScrollerScrollable()",[517,1196,1199],{"className":1197,"code":1198,"language":1143,"meta":523},[1141],"const scrollable = useMessageScrollerScrollable()\n// scrollable.value.start, scrollable.value.end\n",[481,1200,1198],{"__ignoreMap":523},[477,1202,1203,1204,1207],{},"Reports which edges the viewport can still scroll toward. For styling the scroller itself, prefer the ",[481,1205,1206],{},"data-scrollable"," attribute.",{"title":523,"searchDepth":1209,"depth":1209,"links":1210},2,[1211,1212,1213,1214,1215,1228],{"id":474,"depth":1209,"text":475},{"id":494,"depth":1209,"text":295},{"id":552,"depth":1209,"text":553},{"id":568,"depth":1209,"text":569},{"id":580,"depth":1209,"text":581,"children":1216},[1217,1219,1220,1221,1222,1223,1224,1225,1226,1227],{"id":585,"depth":1218,"text":586},3,{"id":614,"depth":1218,"text":615},{"id":625,"depth":1218,"text":626},{"id":640,"depth":1218,"text":641},{"id":658,"depth":1218,"text":659},{"id":673,"depth":1218,"text":674},{"id":692,"depth":1218,"text":693},{"id":710,"depth":1218,"text":711},{"id":735,"depth":1218,"text":736},{"id":758,"depth":1218,"text":759},{"id":772,"depth":1209,"text":773,"children":1229},[1230,1231,1232,1233,1234],{"id":776,"depth":1218,"text":721},{"id":912,"depth":1218,"text":680},{"id":964,"depth":1218,"text":965},{"id":1018,"depth":1218,"text":1019},{"id":1131,"depth":1218,"text":1132},"A scroll container for chat transcripts that anchors turns, follows streamed replies, restores prepended history, and jumps to messages.","md",null,{"component":36},"---\ntitle: Message Scroller\ndescription: A scroll container for chat transcripts that anchors turns, follows streamed replies, restores prepended history, and jumps to messages.\ncomponent: true\nnew: true\n---\n\n::component-preview\n---\nname: MessageScrollerDemo\nclass: style-luma\npreviewClass: theme-green bg-surface dark:bg-background\n---\n::\n\n## MessageScroller\n\nA great streaming chat scroller has to juggle a lot at once: pin to the live edge while a reply streams, but never fight a reader who scrolls up; anchor each new turn near the top with a peek of the previous exchange; preserve position when older history loads above; and expose commands to jump anywhere in the thread. `MessageScroller` owns those hard parts so your message list doesn't have to.\n\nIt does **not** own your messages, AI state, transport, or model — it is a headless scroll container you compose around your own rows.\n\n## Installation\n\n::code-tabs\n\n::tabs-list\n\n  ::tabs-trigger{value=\"cli\"}\n  CLI\n  ::\n\n  ::tabs-trigger{value=\"manual\"}\n  Manual\n  ::\n\n::\n\n::tabs-content{value=\"cli\"}\n\n```bash\nnpx shadcn-vue@latest add message-scroller\n```\n\n::\n\n::tabs-content{value=\"manual\"}\n  ::steps\n    ::step\n    Copy and paste the [GitHub source code](https://github.com/unovue/shadcn-vue/tree/dev/apps/v4/registry/bases/reka/ui/message-scroller) into your project.\n    ::\n\n    ::step\n    Update the import paths to match your project setup.\n    ::\n  ::\n::\n\n::\n\n## Usage\n\n```vue showLineNumbers\n\u003Cscript setup lang=\"ts\">\nimport {\n  MessageScroller,\n  MessageScrollerButton,\n  MessageScrollerContent,\n  MessageScrollerItem,\n  MessageScrollerProvider,\n  MessageScrollerViewport,\n} from '@/components/ui/message-scroller'\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CMessageScrollerProvider auto-scroll default-scroll-position=\"last-anchor\">\n    \u003CMessageScroller>\n      \u003CMessageScrollerViewport>\n        \u003CMessageScrollerContent>\n          \u003CMessageScrollerItem\n            v-for=\"message in messages\"\n            :key=\"message.id\"\n            :message-id=\"message.id\"\n            :scroll-anchor=\"message.role === 'user'\"\n          >\n            \u003C!-- Message / Bubble / Marker goes here -->\n          \u003C/MessageScrollerItem>\n        \u003C/MessageScrollerContent>\n      \u003C/MessageScrollerViewport>\n      \u003CMessageScrollerButton direction=\"end\" />\n    \u003C/MessageScroller>\n  \u003C/MessageScrollerProvider>\n\u003C/template>\n```\n\nThe provider must have a constrained height (or a height-bounded parent) so the viewport can scroll.\n\n## Composition\n\n```text\nMessageScrollerProvider\n└── MessageScroller\n    ├── MessageScrollerViewport\n    │   └── MessageScrollerContent\n    │       └── MessageScrollerItem\n    └── MessageScrollerButton\n```\n\n## Core Concepts\n\n### Anchoring Turns\n\nA turn is the part of the conversation that starts a new exchange — usually the user's message and the assistant reply that follows. An *anchor* is the row the viewport should treat as the start of that turn. Mark that row with `scrollAnchor`. When a new anchor is appended, the viewport moves it near the top and keeps a peek of the previous item above it, so the new turn does not feel detached from its context.\n\n```vue\n\u003CMessageScrollerItem\n  :message-id=\"message.id\"\n  :scroll-anchor=\"message.role === 'user'\"\n>\n  \u003C!-- ... -->\n\u003C/MessageScrollerItem>\n```\n\nScroll anchors are not tied to message role. You can turn any row into an anchor: a user message, a system marker, a handoff event, or anything else that starts a meaningful turn.\n\n::component-preview\n---\nname: MessageScrollerAnchoringDemo\nclass: style-luma\npreviewClass: theme-green bg-surface dark:bg-background\n---\n::\n\n### Group Chat\n\nIn a group chat, the turn boundary is often the message that asks the model to respond, or a marker like \"Marcus joined the chat\". Typing indicators and history controls usually should not anchor. Because anchoring is role-independent, you can anchor a marker just as easily as a message.\n\n::component-preview\n---\nname: MessageScrollerGroupChatDemo\nclass: style-luma\npreviewClass: theme-green bg-surface dark:bg-background\n---\n::\n\n### Keeping Context Visible\n\nWhen a new turn starts, it should still feel like part of the same continuous thread. `scrollPreviousItemPeek` keeps a slice of the previous item visible above the anchor, so the reader keeps their context instead of feeling like the conversation restarted on a blank page.\n\n::component-preview\n---\nname: MessageScrollerPreviousContextDemo\nclass: style-luma\npreviewClass: theme-green bg-surface dark:bg-background\n---\n::\n\n### Following the Live Edge\n\nWhen the reader is at the live edge, `autoScroll` keeps streamed replies in view as they grow. Scrolling away from the live edge — by wheel, touch, keyboard, or dragging the scrollbar — releases the view, so new chunks arrive without moving the reader. `autoScroll` composes with turn anchoring: when a new turn anchors near the top, the view stays put while the reply streams into the room below it.\n\n::component-preview\n---\nname: MessageScrollerStreamingDemo\nclass: style-luma\npreviewClass: theme-green bg-surface dark:bg-background\n---\n::\n\n### Opening Saved Threads\n\nReopening a saved thread at the absolute end often drops the reader in without enough context. A better default is `\"last-anchor\"`: show the last meaningful turn, like the user's latest message, with the reply below it.\n\n::component-preview\n---\nname: MessageScrollerOpeningPositionDemo\nclass: style-luma\npreviewClass: theme-green bg-surface dark:bg-background\n---\n::\n\n### Loading Earlier Messages\n\nLoading earlier messages should not move the conversation the reader is already looking at. When older rows are prepended above the current transcript, `MessageScrollerViewport` preserves the visible row so the reader stays in the same place while history loads above them. This is enabled by default through `preserveScrollOnPrepend`.\n\n::component-preview\n---\nname: MessageScrollerLoadHistoryDemo\nclass: style-luma\npreviewClass: theme-green bg-surface dark:bg-background\n---\n::\n\n### Animating New Messages\n\nA common chat pattern is to animate the user's message when it is sent, then let the assistant reply stream into a regular row below it. Keep `messageId` and `scrollAnchor` on the animated item and use transform and opacity for the entrance — avoid animating height, margin, or padding, which can fight the scroller's positioning.\n\n::component-preview\n---\nname: MessageScrollerAnimationDemo\nclass: style-luma\npreviewClass: theme-green bg-surface dark:bg-background\n---\n::\n\n### Jumping to Messages\n\nSearch results, permalinks, outline items, and toolbar buttons often need to drive the transcript from outside the message list. Use `useMessageScroller` for those controls — the composables read from `MessageScrollerProvider`, so they work in any component inside the provider.\n\n```vue\n\u003Cscript setup lang=\"ts\">\nimport { useMessageScroller } from '@/components/ui/message-scroller'\n\nconst { scrollToMessage, scrollToEnd, scrollToStart } = useMessageScroller()\n\u003C/script>\n```\n\n::component-preview\n---\nname: MessageScrollerCommandsDemo\nclass: style-luma\npreviewClass: theme-green bg-surface dark:bg-background\n---\n::\n\n### Tracking the Reader's Position\n\nUse `useMessageScrollerVisibility` to track the reader's position — a table-of-contents or jump menu that highlights the current anchored turn. `currentAnchorId` answers \"where am I\" and stays set after that anchor scrolls above the viewport; `visibleMessageIds` answers \"what is on screen\", in document order.\n\n::component-preview\n---\nname: MessageScrollerVisibilityDemo\nclass: style-luma\npreviewClass: theme-green bg-surface dark:bg-background\n---\n::\n\n### Reading Scroll State\n\nUse `useMessageScrollerScrollable` when you need scroll state in JavaScript, such as a status indicator or a custom \"jump to latest\" control. It reports which edges the viewport can still scroll toward.\n\n::component-preview\n---\nname: MessageScrollerScrollableDemo\nclass: style-luma\npreviewClass: theme-green bg-surface dark:bg-background\n---\n::\n\n## API Reference\n\n### MessageScrollerProvider\n\nOwns the scroll state and behavior. Provide it via `provide`/`inject` and expose the scroll composables to descendants.\n\n| Prop                     | Type                                  | Default        | Description                                                            |\n| ------------------------ | ------------------------------------- | -------------- | -------------------------------------------------------------------- |\n| `autoScroll`             | `boolean`                             | `false`        | Follow the live edge while the reader is pinned to the bottom.        |\n| `defaultScrollPosition`  | `'start' \\| 'end' \\| 'last-anchor'`   | `'end'`        | Opening position for the transcript.                                  |\n| `scrollEdgeThreshold`    | `number`                              | `8`            | Distance in px from an edge before it is considered scrollable.       |\n| `scrollPreviousItemPeek` | `number`                              | `64`           | Amount in px of the previous turn kept visible when anchoring.        |\n| `scrollMargin`           | `number`                              | `0`            | Extra offset in px applied when scrolling to an element.              |\n\n### MessageScrollerViewport\n\n| Prop                       | Type      | Default | Description                                             |\n| -------------------------- | --------- | ------- | ------------------------------------------------------- |\n| `preserveScrollOnPrepend`  | `boolean` | `true`  | Keep the current view when messages are added above.    |\n\nRendered as a `role=\"region\"`, `aria-label=\"Messages\"`, focusable (`tabindex=\"0\"`) native scroll container.\n\n### MessageScrollerItem\n\n| Prop           | Type      | Default | Description                                      |\n| -------------- | --------- | ------- | ------------------------------------------------ |\n| `messageId`    | `string`  | —       | Stable id used for anchoring, visibility, jumps. |\n| `scrollAnchor` | `boolean` | `false` | Marks this row as the start of a turn.           |\n\n### MessageScrollerButton\n\n| Prop        | Type                  | Default   | Description                              |\n| ----------- | --------------------- | --------- | ---------------------------------------- |\n| `direction` | `'start' \\| 'end'`    | `'end'`   | Direction the button scrolls toward.     |\n| `behavior`  | `ScrollBehavior`      | `'smooth'`| Scroll behavior for the jump.            |\n| `variant`   | `ButtonVariants`      | `'secondary'` | Button variant.                      |\n| `size`      | `ButtonVariants`      | `'icon-sm'`   | Button size.                         |\n\nExposes `data-active` for styling and becomes `inert` with `tabindex=\"-1\"` when there is nothing to scroll toward.\n\n### Composables\n\n#### useMessageScroller()\n\n```ts\nconst { scrollToMessage, scrollToEnd, scrollToStart } = useMessageScroller()\n```\n\n- `scrollToMessage(id, options?)` — scroll to the item with the matching `messageId`. Returns `true` if handled (queued if the item is not mounted yet), `false` if the id is missing after rows have mounted.\n- `scrollToEnd(options?)` / `scrollToStart(options?)` — scroll to the live edge or the top.\n\n#### useMessageScrollerVisibility()\n\n```ts\nconst visibility = useMessageScrollerVisibility()\n// visibility.value.currentAnchorId, visibility.value.visibleMessageIds\n```\n\nTracking only runs while something subscribes, and rows need a `messageId` to participate.\n\n#### useMessageScrollerScrollable()\n\n```ts\nconst scrollable = useMessageScrollerScrollable()\n// scrollable.value.start, scrollable.value.end\n```\n\nReports which edges the viewport can still scroll toward. For styling the scroller itself, prefer the `data-scrollable` attribute.\n",{"title":170,"description":1235},"u_bdwpHf1hlaa0QJ3GukyOS6_lkT8iVHoGCmAk6nsgE",[1243,1244],{"title":166,"path":167,"stem":168,"children":-1},{"title":174,"path":175,"stem":176,"children":-1},1783698905119]