(function (root, factory) {
  if (typeof module !== 'undefined' && module.exports) module.exports = factory;
  else root.createMobilePullRefresh = factory;
})(typeof self !== 'undefined' ? self : this, function createMobilePullRefresh(options) {
  const surface = options.surface || document.querySelector('#my-work');
  const indicator = options.indicator || document.querySelector('#mobile-pull-refresh');
  const threshold = options.threshold || 64;
  const isMobile = options.isMobile || (() => window.matchMedia('(max-width: 600px)').matches);
  const getScrollY = options.getScrollY || (() => window.scrollY);
  const isBlocked = options.isBlocked || (() => Array.from(options.overlays || document.querySelectorAll('[role="dialog"], #whiteboard-modal, #markdown-modal')).some(overlay =>
      (overlay.checkVisibility ? overlay.checkVisibility() : !overlay.closest('[hidden]')) &&
      (overlay.classList.contains('open') || overlay.getAttribute('aria-modal') === 'true')));
  let gesture = null;
  let inFlight = null;

  function show(state, text) {
    indicator.hidden = false;
    indicator.dataset.state = state;
    indicator.textContent = text;
  }

  function resetGesture() {
    gesture = null;
  }

  function startsOnExcludedTarget(target) {
    if (!target) return false;
    if (target.closest?.('button, a, input, textarea, select, summary, [contenteditable="true"], [role="button"]')) return true;
    for (let node = target; node && node !== surface; node = node.parentElement) {
      if ((node.scrollWidth || 0) > (node.clientWidth || 0)) return true;
    }
    return false;
  }

  function onPointerDown(event) {
    if (inFlight || event.pointerType !== 'touch' || !isMobile() || getScrollY() > 0 || isBlocked() || startsOnExcludedTarget(event.target)) return;
    gesture = {id:event.pointerId, x:event.clientX, y:event.clientY, ready:false};
    surface.setPointerCapture?.(event.pointerId);
  }

  function onPointerMove(event) {
    if (!gesture || gesture.id !== event.pointerId) return;
    const dx = event.clientX - gesture.x;
    const dy = event.clientY - gesture.y;
    if (dy <= 0 || Math.abs(dx) > dy) return resetGesture();
    event.preventDefault();
    gesture.ready = dy >= threshold;
    show(gesture.ready ? 'ready' : 'pulling', gesture.ready ? 'Release to refresh' : 'Pull to refresh');
  }

  async function onPointerUp(event) {
    if (!gesture || gesture.id !== event.pointerId) return;
    const shouldRefresh = gesture.ready;
    resetGesture();
    surface.releasePointerCapture?.(event.pointerId);
    if (!shouldRefresh || inFlight) return;
    show('refreshing', 'Refreshing My Work…');
    inFlight = Promise.resolve().then(options.refresh);
    try {
      await inFlight;
      show('success', 'My Work is up to date');
    } catch (_) {
      show('error', 'Could not refresh · pull to retry');
    } finally {
      inFlight = null;
    }
  }

  function onPointerCancel() {
    resetGesture();
  }

  return {
    start() {
      surface.addEventListener('pointerdown', onPointerDown);
      surface.addEventListener('pointermove', onPointerMove);
      surface.addEventListener('pointerup', onPointerUp);
      surface.addEventListener('pointercancel', onPointerCancel);
    },
    stop() {
      surface.removeEventListener('pointerdown', onPointerDown);
      surface.removeEventListener('pointermove', onPointerMove);
      surface.removeEventListener('pointerup', onPointerUp);
      surface.removeEventListener('pointercancel', onPointerCancel);
      resetGesture();
    },
  };
});
