## message_scroller (`poetry_message_scroller`)

A streaming-aware transcript that keeps the latest message in view.

Class: Poetry::Ui::MessageScroller::Component - BEM block `poetry-ui-message_scroller`.
- `auto_scroll:` (boolean) - default true - Follows the newest message while the reader sits at the bottom; scrolling up releases the follow.
- `default_scroll_position:` (symbol) - one of start|end|last-anchor, default "end" - Where the viewport lands on connect: the newest message (:end), the oldest (:start), or the last anchor: true row (:"last-anchor").
- `id:` (string) - required - The transcript's stable identifier - the content element renders dom id "<id>-messages" for Turbo Streams to target.
- `jump_button:` (boolean) - default true - Renders the floating jump-to-latest button (shown once the reader leaves the bottom).
- `preserve_scroll_on_prepend:` (boolean) - default true - Keeps the reading position stable when history prepends into the content element.
- `track_visibility:` (boolean) - default false - Opt-in observation of which rows are on screen - emits a visibility event as the visible set changes.
- PART `message-scroller` - The transcript root the controller drives - runtime scroll state is mirrored here | states: data-mode=following-bottom|free-scrolling|anchored-to-message|settling-jump (always once connected - the 4-state machine); data-scrollable (overflow exists - carries which edges have room (start, end, or both as a space-separated pair)); data-autoscrolling (a programmatic scroll is settling - the follow-bottom release is suppressed while set)
- PART `message-scroller-viewport` - The native scroll region (role=region, focusable) - the controller mirrors the same runtime attributes here | states: data-scrollable (overflow exists - the same edge tokens as the root); data-autoscrolling (a programmatic scroll is settling)
- PART `message-scroller-content` - The row container and Turbo Stream append target (stable dom id <id>-messages); role=log announces additions
- PART `message-scroller-item` - One transcript row (poetry_message_scroller_item) - the id is how anchoring and Streams find it | states: data-message-id (always - the row's message id); data-scroll-anchor (anchor: true - the turn the controller holds at the reading line)
- PART `message-scroller-spacer` - Tail spacer faking scroll room below a short anchored turn - hidden at height 0
- WIRING root: `poetry--core--message-scroller` registers; values auto_scroll, default_scroll_position, preserve_scroll_on_prepend, track_visibility
- WIRING viewport: `poetry--core--message-scroller` targets viewport
- WIRING content: `poetry--core--message-scroller` targets content
- WIRING spacer: `poetry--core--message-scroller` targets spacer
- WIRING jump_button: `poetry--core--message-scroller` actions scrollToEnd; targets button
- RULE: Stream by UPDATING a row's text (morph/replace) - appending nodes per token re-announces the row to AT.
- RULE: Rows are poetry_message_scroller_item(id: message.id) - the id is how anchoring and Streams find them.
- RULE: Append new turns with a Turbo Stream targeting the content element's dom id.
- RULE: History loads PREPEND into the content element - the controller preserves the reading position.
- RULE: Never nest a second scroll container inside the viewport.
