/**
 * Primary phase. Mutually exclusive — exactly one is active at any time.
 *
 * - `idle`            — No active conversation; landing surface.
 * - `switch-pending`  — A conversation switch (or initial load with an
 *                       existing conversation id) was requested. Waiting
 *                       for the messages fetch to resolve.
 * - `switch-settling` — Fetch resolved. Layout is settling; the controller
 *                       is actively snapping the viewport to the bottom and
 *                       waiting for late-arriving content (images, async
 *                       markdown rendering, feedback widgets) to commit.
 * - `ready`           — Steady state: pin, streaming follow, scroll-to-
 *                       bottom button, and pagination all behave per their
 *                       own side-channel state.
 */
export type ScrollPhase = "idle" | "switch-pending" | "switch-settling" | "ready";
/**
 * Active user-message anchor. Set when the user sends a message, cleared
 * when a conversation switch begins or the message is replaced. The
 * controller pins the referenced message near the top of the viewport.
 *
 * `expansionTargetTop` is the synthetic min-height target applied to
 * `.conversation-section` when natural content does not provide enough
 * scroll room to honor the pin. `null` means no expansion is active.
 */
export type UserPinAnchor = {
    messageId: string;
    expansionTargetTop: number | null;
};
/**
 * Scroll position captured at the start of an older-messages load. Used
 * after the prepend lands to restore the user's viewport so the visible
 * messages do not appear to jump.
 */
export type OlderLoadCapture = {
    savedScrollTop: number;
    savedScrollHeight: number;
};
export type ScrollState = {
    readonly phase: ScrollPhase;
    readonly streamingFollowEngaged: boolean;
    readonly userPin: UserPinAnchor | null;
    readonly userPinApplied: boolean;
    readonly olderLoad: OlderLoadCapture | null;
    readonly programmaticLockActive: boolean;
};
export declare const initialScrollState: ScrollState;
export type ScrollAction = {
    type: "CONVERSATION_SWITCH_REQUESTED";
} | {
    type: "CONVERSATION_LOAD_RESOLVED";
} | {
    type: "SWITCH_SETTLE_COMPLETE";
} | {
    type: "USER_MESSAGE_ANCHORED";
    messageId: string;
} | {
    type: "USER_PIN_APPLIED";
    expansionTargetTop: number | null;
} | {
    type: "USER_PIN_EXPANSION_CLEARED";
} | {
    type: "USER_PIN_CLEARED";
} | {
    type: "STREAMING_FOLLOW_ENGAGED";
} | {
    type: "STREAMING_FOLLOW_DISENGAGED";
} | {
    type: "STREAMING_ENDED";
} | {
    type: "PROGRAMMATIC_LOCK_ENGAGED";
} | {
    type: "PROGRAMMATIC_LOCK_RELEASED";
} | {
    type: "OLDER_LOAD_STARTED";
    savedScrollTop: number;
    savedScrollHeight: number;
} | {
    type: "OLDER_LOAD_RESOLVED";
};
/**
 * Apply an action to the state. Pure function — no I/O. Every transition is
 * named; unreachable transitions return state unchanged.
 *
 * INVARIANT: `userPinApplied` only becomes true via `USER_PIN_APPLIED`, and
 * is reset to false on every `USER_MESSAGE_ANCHORED` and switch lifecycle
 * action. The controller relies on this for the "pin only once per
 * message" guarantee.
 */
export declare function scrollReducer(state: ScrollState, action: ScrollAction): ScrollState;
export type ChatSurfaceState = "initial-loading" | "switch-loading" | "ready";
/**
 * Externally observable scroll state value, emitted as `data-scroll-state`
 * on the scroll wrapper. Order of precedence: switch lifecycle > interaction
 * mode > steady. Stable string set documented for downstream consumers
 * (tests, Playwright selectors, Clarity recordings).
 */
export type ScrollStateAttribute = "idle" | "switch-pending" | "switch-settling" | "streaming-follow" | "user-pin-active" | "ready";
export declare function selectScrollStateAttribute(state: ScrollState): ScrollStateAttribute;
export declare function selectChatSurfaceState(args: {
    state: ScrollState;
    loading: boolean;
    isActiveChat: boolean;
    showConversationList: boolean;
}): ChatSurfaceState;
/**
 * Whether the scroll-to-bottom button should be visible. Pure: caller
 * measures the DOM and passes the values in.
 *
 * Hidden during switch lifecycle (the user is transitioning conversations,
 * showing the button would flash). Hidden while the only "distance to
 * bottom" comes from synthetic pin expansion (not real new content).
 * Otherwise visible iff `distanceToBottom > threshold`.
 */
export declare function selectShowScrollToBottom(args: {
    state: ScrollState;
    scrollHeight: number;
    clientHeight: number;
    scrollTop: number;
    isStreaming: boolean;
    threshold: number;
}): boolean;
/**
 * Whether the controller is in the user-visible "transitioning" window
 * (switch-pending or switch-settling). Used by the loading UI and to
 * gate IntersectionObserver pagination triggers.
 */
export declare function selectIsTransitioning(state: ScrollState): boolean;
