/** * PinManager * * Handles element pinning during scroll animations. * Extracted from ScrollTrigger for better separation of concerns. * * Two pinning strategies based on scroller type: * 1. Window scroller: position:fixed (seamless, no jitter) * 2. Custom scroller: translate3d (counters scroll movement) * * State machine: before → pinned → after */ export type PinState = 'before' | 'pinned' | 'after'; export declare class PinManager { private static _registry; private static _users; /** * Get or create a shared PinManager for the given element. * If another ScrollTrigger already pinned this element, returns that same * PinManager so no additional spacer is created. Tracks `id` as a user. */ static getOrCreate(id: number, element: HTMLElement): PinManager; /** * Release a ScrollTrigger's reference to a pinned element's PinManager. * When the last user releases, cleanup() is called and registry entries removed. */ static release(id: number, element: HTMLElement): void; private _id; private _element; private _originalStyles; private _spacer; private _currentState; private _pinStart; private _pinEnd; private _pinDistance; private _useFixedPin; private _pinSpacing; private _elementHeight; private _fixedTop; private _fixedLeft; private _width; private _gridPlacement; private _fixedBreakingAncestors; private _isBodyPin; private _originalHtmlHeight; private _originalHtmlOverflow; constructor(id: number); /** * Setup pinning for an element * * @param element - The element to pin * @param pinStart - Scroll position where pin starts * @param pinEnd - Scroll position where pin ends * @param scrollerStartOffset - Viewport offset for fixed positioning * @param useFixedPin - true for window scroller (position:fixed), false for custom (transform) */ setup(element: HTMLElement, pinStart: number, pinEnd: number, scrollerStartOffset: number, useFixedPin: boolean, pinSpacing?: boolean | 'margin' | 'padding'): void; /** * Measure the element's layout rect for spacer + pin sizing, recovering a real * size when the element has collapsed to 0 in normal flow. * * Normally this is just `getBoundingClientRect()`. But an element whose box is * driven entirely by percentage/auto dimensions with no definite-size ancestor * — e.g. an inline `` that has only a `viewBox` (no width/height attrs) * and `width: 100%`, living in a shrink-to-fit flex column — collapses to a 0 * width (and/or height) in normal flow. The spacer and the pinned element would * then lock that 0, leaving the element invisible: any scroll animation on it * (e.g. `scale`) still runs, but on a 0×0 box, so nothing shows. * * GSAP's `pinSpacing` sidesteps this by wrapping the pinned element in a div * with a FIXED width/height to match its rendered size. To achieve the same, * when the in-flow measurement is degenerate (0 width or height) we briefly * take the element out of the shrink-to-fit flow (`position: absolute`, hidden * to avoid any flash) so its percentage/`max-*` dimensions resolve against a * real containing block, read the recovered rect, then restore the exact prior * inline styles. The recovery is used only when it actually yields a non-zero * box; otherwise the original rect is returned unchanged. * * Non-degenerate elements skip the recovery entirely, so normal pins are * byte-identical to before. */ private _measurePinRect; /** * Update pin state based on scroll position */ update(scrollTop: number): void; /** * Clean up pin - restore original styles and remove spacer */ cleanup(): void; /** * Get the pinned element */ getElement(): HTMLElement | null; /** * Get the pin distance (scroll range over which element is pinned) */ getPinDistance(): number; /** * Get current pin state */ getState(): PinState; /** * Get a layout rect for trigger-position calculations. * * Used by ScrollTrigger._calculateTriggerPositions() and refresh() to obtain the * correct document-relative position of the pinned element when it is position:fixed * — querying the element itself in that state returns viewport-relative coordinates, * which produce incorrect trigger positions. * * Returns a synthetic rect that combines: * - top/left/width from the spacer (document-flow position) * - height from _elementHeight (the element's natural content height) * * The spacer's own BCR height is NOT used because it includes padding-bottom * (= pinDistance, added for pin spacing), which would inflate callers' end-position * calculations and grow the pinDistance on every refresh cycle. * * Returns `null` when the spacer is not in the DOM (transform-pin strategy or * before first setup), signalling the caller to fall back to the element's own rect. */ getLayoutRect(): { top: number; left: number; width: number; height: number; bottom: number; right: number; } | null; /** * Whether this manager is using the body-pin strategy (html height extension). */ isBodyPin(): boolean; /** * Temporarily reset html.style.height to its original (pre-pin) value so that * callers can measure the body's natural height without the inflation caused * by the pin spacing. Returns a restore function that re-applies the current * (inflated) height. No-op for non-body pins. * * Used by ScrollTrigger.refresh() before _calculateTriggerPositions() so * trigger positions are derived from the body's real content height. */ suspendHtmlHeight(): (() => void) | null; /** * Check if using fixed pin strategy */ isFixedPin(): boolean; /** * Detect ancestor elements with CSS properties that break position:fixed. * * Any of these on an ancestor creates a new containing block, causing * position:fixed to position relative to that ancestor instead of the * viewport — making pinned elements scroll off-screen: * * - filter (even blur(0px)) — set by animation libraries as initial state * - transform (even translate(0)) — set by smooth scroll libraries (Lenis) * - will-change: transform/filter/perspective * - perspective * - contain: paint/layout/strict/content * - backdrop-filter * - overflow: clip — clips fixed descendants at element's padding box * * Only needs to run once during first setup (not on refresh). */ private _detectFixedBreakingAncestors; /** * Override properties on ancestors that break position:fixed. * * IMPORTANT: Captures the current inline style value RIGHT BEFORE overriding, * not at detection time. This prevents restoring stale values — e.g. if an * animation set `filter: blur(5px)` at detection time but has since completed * and the SDK cleaned it to `none`, we'd wrongly restore `blur(5px)` on unpin. * * Safe fallback values: * - filter/transform/perspective → 'none' * - will-change → 'auto' * - contain → 'none' * - overflow-x:clip → 'hidden' (still prevents horizontal scrollbar) * - overflow-y:clip → 'visible' */ private _overrideFixedBreakingAncestors; /** * Restore original values on ancestors that were overridden during pin. * The values restored are the ones captured at override time (pin entry), * not detection time — so they reflect the element's actual state just * before the pin started. */ private _restoreFixedBreakingAncestors; /** * Resolve pinSpacing config to a concrete value. * * Auto-detection rules (when pinSpacing is undefined or true): * - Body pin: scroll room is handled via documentElement height, not a spacer. * Return false so the (non-existent) spacer gets no extra padding/margin. * - Everything else: default to 'padding' (adds pinDistance as padding-bottom). * Works reliably in both flex and non-flex contexts with content-box sizing. * * Flex-aware behavior is handled separately in setup() by adding flex-shrink: 0 * to the spacer when inside a flex container, preventing the flex algorithm from * collapsing the spacer below its needed size (GSAP-compatible). */ private _resolvePinSpacing; private _applyFixedState; /** * Fixed positioning for window scroller (seamless pinning) * * Wrapper pattern: the spacer wraps the element at all times (created in * setup, removed in cleanup). State changes only toggle styles on the * element — no spacer insertion/removal between states. * * Layout math (wrapper): * Spacer content-height = elementHeight * Spacer padding-bottom = pinDistance (when pinSpacing enabled) * Spacer total box = elementHeight + pinDistance * * 'before': element in flow inside spacer at top — no offset * 'pinned': element position:fixed, spacer holds space * 'after': element back in flow at spacer top, translateY(pinDistance) * moves it visually to (originalPos + pinDistance) — seamless * transition with the next section */ private _updateFixed; /** * Transform-based pinning for custom scroll containers */ private _updateTransform; }