# `Minga.Editing.Scroll`
[🔗](https://github.com/jsmestad/minga/blob/main/lib/minga/editing/scroll.ex#L1)

Generic scroll state for any content region that can be scrolled.

Encapsulates a three-part model:

  * `offset` — concrete line count from the top of the content.
    Always a real number, never a sentinel.
  * `pinned` — boolean flag meaning "follow the bottom."
    When true, the renderer ignores `offset` and computes the
    bottom position from the actual content dimensions.
  * `metrics` — cached `{total_lines, visible_height}` from the
    most recent render pass. Updated by the render pipeline after
    every frame so that `scroll_up/2` and `scroll_down/2` can
    resolve a pinned position into a concrete offset without the
    caller passing dimensions.

## Usage

Embed a `%Scroll{}` in any struct that needs scrollable content:

    defstruct scroll: Scroll.new()

The render pipeline must call `update_metrics/3` after computing
content dimensions. Scroll functions are then self-sufficient:
every caller just calls `scroll_up/2` or `scroll_down/2` without
passing content dimensions or calling a materialization step.

# `metrics`

```elixir
@type metrics() :: %{total_lines: non_neg_integer(), visible_height: pos_integer()}
```

Cached metrics from the most recent render pass.

Updated by the render pipeline after each frame. Between frames no
scroll commands execute, so the cache is always fresh when it matters.

# `t`

```elixir
@type t() :: %Minga.Editing.Scroll{
  metrics: metrics(),
  offset: non_neg_integer(),
  pinned: boolean()
}
```

Scroll state for a content region.

# `new`

```elixir
@spec new() :: t()
```

Creates a new scroll state, pinned to bottom.

# `new`

```elixir
@spec new(non_neg_integer()) :: t()
```

Creates a new scroll state starting at a specific offset, unpinned.

# `pin_to_bottom`

```elixir
@spec pin_to_bottom(t()) :: t()
```

Pins to the bottom. The renderer resolves this to a concrete line
number at render time using the actual content dimensions.

# `resolve`

```elixir
@spec resolve(t(), non_neg_integer(), pos_integer()) :: non_neg_integer()
```

Resolves the effective scroll offset for rendering.

When pinned, computes `max(total_lines - visible_height, 0)`.
When unpinned, clamps `offset` to the valid range.

Renderers call this instead of reading `offset` directly.

# `scroll_down`

```elixir
@spec scroll_down(t(), non_neg_integer()) :: t()
```

Scrolls down by the given number of lines. Unpins from bottom.

When transitioning from pinned, uses cached metrics to compute the
concrete bottom offset before adding. The renderer clamps overshoot,
so unbounded addition is safe.

# `scroll_to_top`

```elixir
@spec scroll_to_top(t()) :: t()
```

Scrolls to the top. Unpins from bottom.

# `scroll_up`

```elixir
@spec scroll_up(t(), non_neg_integer()) :: t()
```

Scrolls up by the given number of lines. Unpins from bottom.

When transitioning from pinned, uses cached metrics to compute the
concrete bottom offset before subtracting.

# `set_offset`

```elixir
@spec set_offset(t(), non_neg_integer()) :: t()
```

Sets the offset to an absolute value. Unpins from bottom.

Used by search navigation, code block jumping, and other features
that need to position the viewport at a specific line.

# `update_metrics`

```elixir
@spec update_metrics(t(), non_neg_integer(), pos_integer()) :: t()
```

Updates the cached metrics from the most recent render pass.

Called by the render pipeline after computing content dimensions.
Must be called every frame so that scroll_up/scroll_down have
accurate dimensions when transitioning from pinned to manual.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
