# ScrollFollowModel

> Headless scroll-follow state for a conversational message viewport: the pinned live edge, the anchored turn, the visible messages, and the reachable scroll edges.

```ts
import { ScrollFollowModel } from "@sdxc/ui/behaviors";
```

Auto-follow state for a message viewport: the pinned live edge, the
anchored turn, the visible messages, and the reachable scroll edges. Every
measurement arrives through a setter, so transitions stay DOM-free.

## Signature

```ts
new ScrollFollowModel(options?: ScrollFollowModel.Options)
```

## Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| `options?` | `ScrollFollowModel.Options` | Initial pinned state, anchor turn, visible messages, and edge reachability. All are optional and default to a freshly pinned model anchored to nothing, with no visible messages and both edges unreachable. |

## Properties

| Property | Type | Description |
| --- | --- | --- |
| `pinned` | `boolean` | Whether the viewport is auto-following the live edge, so arriving messages scroll the reader down. Updated through setPinned. |
| `anchorTurnId` | `string \| null` | Id of the turn the viewport is anchored to, or `null` before one has been measured. Read back to hold the reader's position while older history prepends above this turn. |
| `visibleMessageIds` | `ReadonlySet<string>` | Ids of the messages currently visible in the viewport, as last reported through setMessageVisible. |
| `startReachable` | `boolean` | Whether the viewport can still be scrolled toward its start edge. |
| `endReachable` | `boolean` | Whether the viewport can still be scrolled toward its end edge. |
| `pendingScrollRequest` | `ScrollFollowModel.ScrollRequest \| null` | The scroll intent recorded by scrollToEnd, scrollToStart, or scrollToMessage that is still unfulfilled, or `null` once consumeScrollRequest has cleared it. |

## Methods

| Method | Description |
| --- | --- |
| `setPinned(pinned: boolean): void` | Records whether the viewport is auto-following the live edge, as observed while the reader scrolls. A no-op, dispatching nothing, when `pinned` already matches the current value. |
| `setAnchorTurnId(id: string \| null): void` | Records which turn the viewport is anchored to, once the caller has measured which turn sits nearest the anchor edge. A no-op, dispatching nothing, when `id` already matches the current anchor. |
| `setMessageVisible(id: string, visible: boolean): void` | Records whether a single message is visible, one entry at a time to match an `IntersectionObserver` callback. A no-op, dispatching nothing, when `visible` already matches membership in visibleMessageIds. |
| `isMessageVisible(id: string): boolean` | Reports whether a message is visible, per the last report given to setMessageVisible. The read side of the visibility API, so a consumer reads visibility here after each `"change"`. |
| `setReachableEdges(edges: ScrollFollowModel.ReachableEdges): void` | Records which scrollable edges the viewport can reach. Takes both edges at once because one scroll or resize measurement yields both. A no-op, dispatching nothing, when neither edge's reachability changes. |
| `scrollToEnd(): void` | Records an intent to scroll to the live edge of the conversation. Always dispatches `"change"`, even while a request is pending, so a repeated "jump to latest" click still reaches the viewport. |
| `scrollToStart(): void` | Records an intent to scroll to the start of the conversation. Always dispatches `"change"`, even when a request is already pending. |
| `scrollToMessage(id: string, options?: ScrollFollowModel.ScrollToMessageOptions): void` | Records an intent to scroll to a specific message. Always dispatches `"change"`, even when a request is already pending. |
| `consumeScrollRequest(): ScrollFollowModel.ScrollRequest \| null` | Reads and clears the pending scroll request in one step, so an intent is fulfilled exactly once. Clearing stays silent — subscribers already reacted when the intent was recorded. |

## Examples

```tsx
let model = new ScrollFollowModel();
model.addEventListener("change", () => update());
model.setReachableEdges({ start: true, end: false });
model.scrollToEnd();
model.consumeScrollRequest(); // { type: "end" }
```

## Used with it

The events, constants and types its module publishes, each imported from `@sdxc/ui/behaviors` too.

### ScrollFollowModel.ScrollToMessageOptions

Options accepted by ScrollFollowModel.scrollToMessage.

#### Members

| Member | Type | Description |
| --- | --- | --- |
| `align?` | `Align` | Where the message lands in the viewport. Defaults to `"start"`. |
| `smooth?` | `boolean` | Whether the viewport animates on its way to the message. Defaults to `true`. |

### ScrollFollowModel.ReachableEdges

Reachability of the two ends of the scrollable region, as last measured.

#### Members

| Member | Type | Description |
| --- | --- | --- |
| `start` | `boolean` | Whether the viewport can still be scrolled toward its start edge. |
| `end` | `boolean` | Whether the viewport can still be scrolled toward its end edge. |

### ScrollFollowModel.Options

Constructor options for ScrollFollowModel.

#### Members

| Member | Type | Description |
| --- | --- | --- |
| `pinned?` | `boolean` | Whether auto-follow starts engaged. Defaults to `true`. |
| `anchorTurnId?` | `string \| null` | Turn id the viewport starts anchored to. Defaults to `null`. |
| `visibleMessageIds?` | `Iterable<string>` | Message ids visible in the viewport at construction. Defaults to none. |
| `reachableEdges?` | `ReachableEdges` | Reachability of the start/end edges at construction. Defaults to both unreachable. |

### ScrollFollowModel.EventMap

Events dispatched by ScrollFollowModel as its state changes.

#### Members

| Member | Type | Description |
| --- | --- | --- |
| `change` | `Event` | Dispatched after any owned state changes, or a scroll intent is recorded. |

### ScrollFollowModel.Align

Where a scrolled-to message should land inside the viewport once an
intent is fulfilled, matching `Element.scrollIntoView`'s `block` option.

#### Signature

```ts
type ScrollFollowModel.Align = "start" | "center" | "end"
```

One of `"start"`, `"center"`, `"end"`.

### ScrollFollowModel.ScrollRequest

One scroll intent recorded by an intent method (`scrollToEnd`,
`scrollToStart`, `scrollToMessage`) and read back by the caller that
fulfills it against the real viewport.

#### Signature

```ts
type ScrollFollowModel.ScrollRequest = | { type: "end" }
		| { type: "start" }
		| { type: "message"; id: string; align: Align; smooth: boolean }
```
