// packages/snap/src/debounce.ts function debounce(callback, delay) { let timer; return function(...args) { let context = this; clearTimeout(timer); timer = setTimeout(() => { timer = void 0; callback.apply(context, args); }, delay); }; } // packages/snap/src/element.ts function removeParentSticky(element) { const position = getComputedStyle(element).position; const isSticky = position === "sticky"; if (isSticky) { element.style.setProperty("position", "static"); element.dataset.sticky = "true"; } if (element.offsetParent) { removeParentSticky(element.offsetParent); } } function addParentSticky(element) { if (element?.dataset?.sticky === "true") { element.style.removeProperty("position"); delete element.dataset.sticky; } if (element.offsetParent) { addParentSticky(element.offsetParent); } } function offsetTop(element, accumulator = 0) { const top = accumulator + element.offsetTop; if (element.offsetParent) { return offsetTop(element.offsetParent, top); } return top; } function offsetLeft(element, accumulator = 0) { const left = accumulator + element.offsetLeft; if (element.offsetParent) { return offsetLeft(element.offsetParent, left); } return left; } function scrollTop(element, accumulator = 0) { const top = accumulator + element.scrollTop; if (element.offsetParent) { return scrollTop(element.offsetParent, top); } return top + window.scrollY; } function scrollLeft(element, accumulator = 0) { const left = accumulator + element.scrollLeft; if (element.offsetParent) { return scrollLeft(element.offsetParent, left); } return left + window.scrollX; } var SnapElement = class { element; options; align; // @ts-ignore rect = {}; wrapperResizeObserver; resizeObserver; debouncedWrapperResize; constructor(element, { align = ["start"], ignoreSticky = true, ignoreTransform = false } = {}) { this.element = element; this.options = { align, ignoreSticky, ignoreTransform }; this.align = [align].flat(); this.debouncedWrapperResize = debounce(this.onWrapperResize, 500); this.wrapperResizeObserver = new ResizeObserver(this.debouncedWrapperResize); this.wrapperResizeObserver.observe(document.body); this.onWrapperResize(); this.resizeObserver = new ResizeObserver(this.onResize); this.resizeObserver.observe(this.element); this.setRect({ width: this.element.offsetWidth, height: this.element.offsetHeight }); } destroy() { this.wrapperResizeObserver.disconnect(); this.resizeObserver.disconnect(); } setRect({ top, left, width, height, element } = {}) { top = top ?? this.rect.top; left = left ?? this.rect.left; width = width ?? this.rect.width; height = height ?? this.rect.height; element = element ?? this.rect.element; if (top === this.rect.top && left === this.rect.left && width === this.rect.width && height === this.rect.height && element === this.rect.element) return; this.rect.top = top; this.rect.y = top; this.rect.width = width; this.rect.height = height; this.rect.left = left; this.rect.x = left; this.rect.bottom = top + height; this.rect.right = left + width; } onWrapperResize = () => { let top, left; if (this.options.ignoreSticky) removeParentSticky(this.element); if (this.options.ignoreTransform) { top = offsetTop(this.element); left = offsetLeft(this.element); } else { const rect = this.element.getBoundingClientRect(); top = rect.top + scrollTop(this.element); left = rect.left + scrollLeft(this.element); } if (this.options.ignoreSticky) addParentSticky(this.element); this.setRect({ top, left }); }; onResize = ([entry]) => { if (!entry?.borderBoxSize[0]) return; const width = entry.borderBoxSize[0].inlineSize; const height = entry.borderBoxSize[0].blockSize; this.setRect({ width, height }); }; }; // packages/snap/src/uid.ts var index = 0; function uid() { return index++; } // packages/snap/src/snap.ts var Snap = class { constructor(lenis, { type = "proximity", lerp, easing, duration, distanceThreshold = "50%", // useless when type is "mandatory" debounce: debounceDelay = 500, onSnapStart, onSnapComplete } = {}) { this.lenis = lenis; this.options = { type, lerp, easing, duration, distanceThreshold, debounce: debounceDelay, onSnapStart, onSnapComplete }; this.onWindowResize(); window.addEventListener("resize", this.onWindowResize, false); this.onSnapDebounced = debounce(this.onSnap, this.options.debounce); this.lenis.on("virtual-scroll", this.onSnapDebounced); } options; elements = /* @__PURE__ */ new Map(); snaps = /* @__PURE__ */ new Map(); viewport = { width: window.innerWidth, height: window.innerHeight }; isStopped = false; onSnapDebounced; currentSnapIndex; /** * Destroy the snap instance */ destroy() { this.lenis.off("virtual-scroll", this.onSnapDebounced); window.removeEventListener("resize", this.onWindowResize, false); this.elements.forEach((element) => element.destroy()); } /** * Start the snap after it has been stopped */ start() { this.isStopped = false; } /** * Stop the snap */ stop() { this.isStopped = true; } /** * Add a snap to the snap instance * * @param value The value to snap to * @param userData User data that will be forwarded through the snap event * @returns Unsubscribe function */ add(value) { const id = uid(); this.snaps.set(id, { value }); return () => this.snaps.delete(id); } /** * Add an element to the snap instance * * @param element The element to add * @param options The options for the element * @returns Unsubscribe function */ addElement(element, options = {}) { const id = uid(); this.elements.set(id, new SnapElement(element, options)); return () => this.elements.delete(id); } addElements(elements, options = {}) { const map = elements.map((element) => this.addElement(element, options)); return () => { map.forEach((remove) => { remove(); }); }; } onWindowResize = () => { this.viewport.width = window.innerWidth; this.viewport.height = window.innerHeight; }; computeSnaps = () => { const { isHorizontal } = this.lenis; let snaps = [...this.snaps.values()]; this.elements.forEach(({ rect, align }) => { let value; align.forEach((align2) => { if (align2 === "start") { value = rect.top; } else if (align2 === "center") { value = isHorizontal ? rect.left + rect.width / 2 - this.viewport.width / 2 : rect.top + rect.height / 2 - this.viewport.height / 2; } else if (align2 === "end") { value = isHorizontal ? rect.left + rect.width - this.viewport.width : rect.top + rect.height - this.viewport.height; } if (typeof value === "number") { snaps.push({ value: Math.ceil(value) }); } }); }); snaps = snaps.sort((a, b) => Math.abs(a.value) - Math.abs(b.value)); return snaps; }; previous() { this.goTo((this.currentSnapIndex ?? 0) - 1); } next() { this.goTo((this.currentSnapIndex ?? 0) + 1); } goTo(index2) { const snaps = this.computeSnaps(); if (snaps.length === 0) return; this.currentSnapIndex = Math.max(0, Math.min(index2, snaps.length - 1)); const currentSnap = snaps[this.currentSnapIndex]; if (currentSnap === void 0) return; this.lenis.scrollTo(currentSnap.value, { duration: this.options.duration, easing: this.options.easing, lerp: this.options.lerp, lock: this.options.type === "lock", userData: { initiator: "snap" }, onStart: () => { this.options.onSnapStart?.({ index: this.currentSnapIndex, ...currentSnap }); }, onComplete: () => { this.options.onSnapComplete?.({ index: this.currentSnapIndex, ...currentSnap }); } }); } get distanceThreshold() { let distanceThreshold = Infinity; if (this.options.type === "mandatory") return Infinity; const { isHorizontal } = this.lenis; const axis = isHorizontal ? "width" : "height"; if (typeof this.options.distanceThreshold === "string" && this.options.distanceThreshold.endsWith("%")) { distanceThreshold = Number(this.options.distanceThreshold.replace("%", "")) / 100 * this.viewport[axis]; } else if (typeof this.options.distanceThreshold === "number") { distanceThreshold = this.options.distanceThreshold; } else { distanceThreshold = this.viewport[axis]; } return distanceThreshold; } onSnap = (e) => { if (this.isStopped) return; if (e.event.type === "touchmove") return; if (this.options.type === "lock" && this.lenis.userData?.initiator === "snap") return; let { scroll, isHorizontal } = this.lenis; const delta = isHorizontal ? e.deltaX : e.deltaY; scroll = Math.ceil(this.lenis.scroll + delta); const snaps = this.computeSnaps(); if (snaps.length === 0) return; let snapIndex; const prevSnapIndex = snaps.findLastIndex(({ value }) => value < scroll); const nextSnapIndex = snaps.findIndex(({ value }) => value > scroll); if (this.options.type === "lock") { if (delta > 0) { snapIndex = nextSnapIndex; } else if (delta < 0) { snapIndex = prevSnapIndex; } } else { const prevSnap = snaps[prevSnapIndex]; const distanceToPrevSnap = prevSnap ? Math.abs(scroll - prevSnap.value) : Infinity; const nextSnap = snaps[nextSnapIndex]; const distanceToNextSnap = nextSnap ? Math.abs(scroll - nextSnap.value) : Infinity; snapIndex = distanceToPrevSnap < distanceToNextSnap ? prevSnapIndex : nextSnapIndex; } if (snapIndex === void 0) return; if (snapIndex === -1) return; snapIndex = Math.max(0, Math.min(snapIndex, snaps.length - 1)); const snap = snaps[snapIndex]; const distance = Math.abs(scroll - snap.value); if (distance <= this.distanceThreshold) { this.goTo(snapIndex); } }; resize() { this.elements.forEach((element) => element.onWrapperResize()); } }; export { Snap as default }; //# sourceMappingURL=lenis-snap.mjs.map