"use strict";
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
const React = require("react");
/*!---------------------------------------------------------------------------------------------
 *  Copyright (c) StackBlitz. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
const DEFAULT_SPRING_ANIMATION = {
  /**
   * A value from 0 to 1, on how much to damp the animation.
   * 0 means no damping, 1 means full damping.
   *
   * @default 0.7
   */
  damping: 0.7,
  /**
   * The stiffness of how fast/slow the animation gets up to speed.
   *
   * @default 0.05
   */
  stiffness: 0.05,
  /**
   * The inertial mass associated with the animation.
   * Higher numbers make the animation slower.
   *
   * @default 1.25
   */
  mass: 1.25
};
const STICK_TO_BOTTOM_OFFSET_PX = 70;
const SIXTY_FPS_INTERVAL_MS = 1e3 / 60;
const RETAIN_ANIMATION_DURATION_MS = 350;
let mouseDown = false;
globalThis.document?.addEventListener("mousedown", () => {
  mouseDown = true;
});
globalThis.document?.addEventListener("mouseup", () => {
  mouseDown = false;
});
globalThis.document?.addEventListener("click", () => {
  mouseDown = false;
});
const useStickToBottom = (options = {}) => {
  const [escapedFromLock, updateEscapedFromLock] = React.useState(false);
  const [isAtBottom, updateIsAtBottom] = React.useState(options.initial !== false);
  const [isNearBottom, setIsNearBottom] = React.useState(false);
  const optionsRef = React.useRef(null);
  optionsRef.current = options;
  const isSelecting = React.useCallback(() => {
    if (!mouseDown) {
      return false;
    }
    const selection = window.getSelection();
    if (!selection || !selection.rangeCount) {
      return false;
    }
    const range = selection.getRangeAt(0);
    return range.commonAncestorContainer.contains(scrollRef.current) || scrollRef.current?.contains(range.commonAncestorContainer);
  }, []);
  const setIsAtBottom = React.useCallback((isAtBottom2) => {
    state.isAtBottom = isAtBottom2;
    updateIsAtBottom(isAtBottom2);
  }, []);
  const setEscapedFromLock = React.useCallback((escapedFromLock2) => {
    state.escapedFromLock = escapedFromLock2;
    updateEscapedFromLock(escapedFromLock2);
  }, []);
  const state = React.useMemo(() => {
    let lastCalculation;
    return {
      escapedFromLock,
      isAtBottom,
      resizeDifference: 0,
      accumulated: 0,
      velocity: 0,
      listeners: /* @__PURE__ */ new Set(),
      get scrollTop() {
        return scrollRef.current?.scrollTop ?? 0;
      },
      set scrollTop(scrollTop) {
        if (scrollRef.current) {
          scrollRef.current.scrollTop = scrollTop;
          state.ignoreScrollToTop = scrollRef.current.scrollTop;
        }
      },
      get targetScrollTop() {
        if (!scrollRef.current || !contentRef.current) {
          return 0;
        }
        return scrollRef.current.scrollHeight - 1 - scrollRef.current.clientHeight;
      },
      get calculatedTargetScrollTop() {
        if (!scrollRef.current || !contentRef.current) {
          return 0;
        }
        const { targetScrollTop } = this;
        if (!options.targetScrollTop) {
          return targetScrollTop;
        }
        if (lastCalculation?.targetScrollTop === targetScrollTop) {
          return lastCalculation.calculatedScrollTop;
        }
        const calculatedScrollTop = Math.max(Math.min(options.targetScrollTop(targetScrollTop, {
          scrollElement: scrollRef.current,
          contentElement: contentRef.current
        }), targetScrollTop), 0);
        lastCalculation = { targetScrollTop, calculatedScrollTop };
        requestAnimationFrame(() => {
          lastCalculation = void 0;
        });
        return calculatedScrollTop;
      },
      get scrollDifference() {
        return this.calculatedTargetScrollTop - this.scrollTop;
      },
      get isNearBottom() {
        return this.scrollDifference <= STICK_TO_BOTTOM_OFFSET_PX;
      }
    };
  }, []);
  const scrollToBottom = React.useCallback((scrollOptions = {}) => {
    if (typeof scrollOptions === "string") {
      scrollOptions = { animation: scrollOptions };
    }
    if (!scrollOptions.preserveScrollPosition) {
      setIsAtBottom(true);
    }
    const waitElapsed = Date.now() + (Number(scrollOptions.wait) || 0);
    const behavior = mergeAnimations(optionsRef.current, scrollOptions.animation);
    const { ignoreEscapes = false } = scrollOptions;
    let durationElapsed;
    let startTarget = state.calculatedTargetScrollTop;
    if (scrollOptions.duration instanceof Promise) {
      scrollOptions.duration.finally(() => {
        durationElapsed = Date.now();
      });
    } else {
      durationElapsed = waitElapsed + (scrollOptions.duration ?? 0);
    }
    const next = async () => {
      const promise = new Promise(requestAnimationFrame).then(() => {
        if (!state.isAtBottom) {
          state.animation = void 0;
          return false;
        }
        const { scrollTop } = state;
        const tick = performance.now();
        const tickDelta = (tick - (state.lastTick ?? tick)) / SIXTY_FPS_INTERVAL_MS;
        state.animation || (state.animation = { behavior, promise, ignoreEscapes });
        if (state.animation.behavior === behavior) {
          state.lastTick = tick;
        }
        if (isSelecting()) {
          return next();
        }
        if (waitElapsed > Date.now()) {
          return next();
        }
        if (scrollTop < Math.min(startTarget, state.calculatedTargetScrollTop)) {
          if (state.animation?.behavior === behavior) {
            if (behavior === "instant") {
              state.scrollTop = state.calculatedTargetScrollTop;
              return next();
            }
            state.velocity = (behavior.damping * state.velocity + behavior.stiffness * state.scrollDifference) / behavior.mass;
            state.accumulated += state.velocity * tickDelta;
            state.scrollTop += state.accumulated;
            if (state.scrollTop !== scrollTop) {
              state.accumulated = 0;
            }
          }
          return next();
        }
        if (durationElapsed > Date.now()) {
          startTarget = state.calculatedTargetScrollTop;
          return next();
        }
        state.animation = void 0;
        if (state.scrollTop < state.calculatedTargetScrollTop) {
          return scrollToBottom({
            animation: mergeAnimations(optionsRef.current, optionsRef.current.resize),
            ignoreEscapes,
            duration: Math.max(0, durationElapsed - Date.now()) || void 0
          });
        }
        return state.isAtBottom;
      });
      return promise.then((isAtBottom2) => {
        requestAnimationFrame(() => {
          if (!state.animation) {
            state.lastTick = void 0;
            state.velocity = 0;
          }
        });
        return isAtBottom2;
      });
    };
    if (scrollOptions.wait !== true) {
      state.animation = void 0;
    }
    if (state.animation?.behavior === behavior) {
      return state.animation.promise;
    }
    return next();
  }, [setIsAtBottom, isSelecting, state]);
  const stopScroll = React.useCallback(() => {
    setEscapedFromLock(true);
    setIsAtBottom(false);
  }, [setEscapedFromLock, setIsAtBottom]);
  const handleScroll = React.useCallback(({ target }) => {
    if (target !== scrollRef.current) {
      return;
    }
    const { scrollTop, ignoreScrollToTop } = state;
    let { lastScrollTop = scrollTop } = state;
    state.lastScrollTop = scrollTop;
    state.ignoreScrollToTop = void 0;
    if (ignoreScrollToTop && ignoreScrollToTop > scrollTop) {
      lastScrollTop = ignoreScrollToTop;
    }
    setIsNearBottom(state.isNearBottom);
    setTimeout(() => {
      if (state.resizeDifference || scrollTop === ignoreScrollToTop) {
        return;
      }
      if (isSelecting()) {
        setEscapedFromLock(true);
        setIsAtBottom(false);
        return;
      }
      const isScrollingDown = scrollTop > lastScrollTop;
      const isScrollingUp = scrollTop < lastScrollTop;
      if (state.animation?.ignoreEscapes) {
        state.scrollTop = lastScrollTop;
        return;
      }
      if (isScrollingUp) {
        setEscapedFromLock(true);
        setIsAtBottom(false);
      }
      if (isScrollingDown) {
        setEscapedFromLock(false);
      }
      if (!state.escapedFromLock && state.isNearBottom) {
        setIsAtBottom(true);
      }
    }, 1);
  }, [setEscapedFromLock, setIsAtBottom, isSelecting, state]);
  const handleWheel = React.useCallback(({ target, deltaY }) => {
    let element = target;
    while (!["scroll", "auto"].includes(getComputedStyle(element).overflow)) {
      if (!element.parentElement) {
        return;
      }
      element = element.parentElement;
    }
    if (element === scrollRef.current && deltaY < 0 && scrollRef.current.scrollHeight > scrollRef.current.clientHeight && !state.animation?.ignoreEscapes) {
      setEscapedFromLock(true);
      setIsAtBottom(false);
    }
  }, [setEscapedFromLock, setIsAtBottom, state]);
  const scrollRef = useRefCallback((scroll) => {
    scrollRef.current?.removeEventListener("scroll", handleScroll);
    scrollRef.current?.removeEventListener("wheel", handleWheel);
    scroll?.addEventListener("scroll", handleScroll, { passive: true });
    scroll?.addEventListener("wheel", handleWheel, { passive: true });
  }, []);
  const contentRef = useRefCallback((content) => {
    state.resizeObserver?.disconnect();
    if (!content) {
      return;
    }
    let previousHeight;
    state.resizeObserver = new ResizeObserver(([entry]) => {
      const { height } = entry.contentRect;
      const difference = height - (previousHeight ?? height);
      state.resizeDifference = difference;
      if (state.scrollTop > state.targetScrollTop) {
        state.scrollTop = state.targetScrollTop;
      }
      setIsNearBottom(state.isNearBottom);
      if (difference >= 0) {
        const animation = mergeAnimations(optionsRef.current, previousHeight ? optionsRef.current.resize : optionsRef.current.initial);
        scrollToBottom({
          animation,
          wait: true,
          preserveScrollPosition: true,
          duration: animation === "instant" ? void 0 : RETAIN_ANIMATION_DURATION_MS
        });
      } else {
        if (state.isNearBottom) {
          setEscapedFromLock(false);
          setIsAtBottom(true);
        }
      }
      previousHeight = height;
      requestAnimationFrame(() => {
        setTimeout(() => {
          if (state.resizeDifference === difference) {
            state.resizeDifference = 0;
          }
        }, 1);
      });
    });
    state.resizeObserver?.observe(content);
  }, []);
  return {
    contentRef,
    scrollRef,
    scrollToBottom,
    stopScroll,
    isAtBottom: isAtBottom || isNearBottom,
    isNearBottom,
    escapedFromLock,
    state
  };
};
function useRefCallback(callback, deps) {
  const result = React.useCallback((ref) => {
    result.current = ref;
    return callback(ref);
  }, deps);
  return result;
}
const animationCache = /* @__PURE__ */ new Map();
function mergeAnimations(...animations) {
  const result = { ...DEFAULT_SPRING_ANIMATION };
  let instant = false;
  for (const animation of animations) {
    if (animation === "instant") {
      instant = true;
      continue;
    }
    if (typeof animation !== "object") {
      continue;
    }
    instant = false;
    result.damping = animation.damping ?? result.damping;
    result.stiffness = animation.stiffness ?? result.stiffness;
    result.mass = animation.mass ?? result.mass;
  }
  const key = JSON.stringify(result);
  if (!animationCache.has(key)) {
    animationCache.set(key, Object.freeze(result));
  }
  return instant ? "instant" : animationCache.get(key);
}
exports.useStickToBottom = useStickToBottom;
//# sourceMappingURL=dist-useStickToBottom.cjs.map
