/*
Maintains packages/share/src/scripts/lenis.ts as an authored share script module so agents can evolve it without rediscovering local boundaries.
- Do not manage app-specific UI states (e.g. scroll-to-top button visibility).
- RFC-1097: step 6 — compass.migrate codemod run
Mechanical v1 to v2 header migration across the workspace: 942 files rewritten — CHANGE_SUMMARY windows collapsed into history, forbidden v1 blocks stripped, KEY_DECISIONS seeded from @ai-invariant comments (5 files) or TODO placeholders (103 files), blocks reordered to canonical order.
- RFC-1097: sweep — werkstatt-engine clean
Sweep batch 4: 73 Compass headers on headerless engine files (certification, component-runtime, isolation, evolution, testing), real KEY_DECISIONS on 75 files (kernel, cache, dht, swim, gitmesh, runtime), ~80 purpose expansions (CONTRACT-02/PURPOSE-02), non-goals on 13 CONTRACT-03 files, CS-07 history literal fix repo-wide (253 files). Policy: .template.ts/.template.astro excludedPaths. werkstatt-engine now 0 diagnostics.
- RFC-1175 AC-5: add data-lenis-initialized marker after the last addEventListener — SMOKE-INIT-01 compliance surfaced when component.smoke.init-contract.validate was revived for the single-chunk orchestrator.
*/
// @ai-invariant: This is a high-risk module. Preserve its core logic and minimize external side effects during modification.
export type LenisInstance = import("lenis").default;
export type InitLenisOptions = {
prefersReducedMotion?: boolean;
headerOffset?: number;
duration?: number;
onScroll?: (lenis: LenisInstance) => void;
};
let lenisInstance: LenisInstance | null = null;
export async function initLenis(
options: InitLenisOptions = {},
): Promise {
const { prefersReducedMotion = false, headerOffset = 0, duration = 1.2, onScroll } = options;
if (prefersReducedMotion) {
(window as Window & { wgLenis?: LenisInstance }).wgLenis = undefined;
return;
}
const Lenis = await import("lenis");
lenisInstance = new Lenis.default({
duration,
easing: (t: number) => Math.min(1, 1.001 - 2 ** (-10 * t)),
orientation: "vertical",
gestureOrientation: "vertical",
smoothWheel: true,
wheelMultiplier: 1,
touchMultiplier: 2,
});
(window as Window & { wgLenis?: LenisInstance }).wgLenis = lenisInstance;
if (onScroll) {
lenisInstance.on("scroll", () => onScroll(lenisInstance!));
}
window.dispatchEvent(
new CustomEvent("lenis:ready", {
detail: { lenis: lenisInstance },
}),
);
const activeLenis = lenisInstance;
// Handle anchor links for smooth scrolling
document.addEventListener("click", (e) => {
const target = e.target as HTMLElement;
const link = target.closest('a[href^="#"]');
if (link) {
const href = link.getAttribute("href");
if (href && href !== "#") {
const targetElement = document.querySelector(href);
if (targetElement) {
// Skip if it's a modal or something that should handle itself
if (targetElement.classList.contains("wl-modal")) {
return;
}
e.preventDefault();
activeLenis.scrollTo(targetElement as HTMLElement, {
offset: -headerOffset,
immediate: prefersReducedMotion,
});
history.replaceState(null, "", href);
}
}
}
});
// Scroll to hash target on page load (cross-page anchor navigation)
if (window.location.hash) {
const hashTarget = document.querySelector(window.location.hash);
if (hashTarget && !hashTarget.classList.contains("wl-modal")) {
// Defer until after the browser's initial scroll attempt
requestAnimationFrame(() => {
activeLenis.scrollTo(hashTarget as HTMLElement, {
offset: -headerOffset,
immediate: prefersReducedMotion,
});
});
}
}
function raf(time: number) {
activeLenis.raf(time);
requestAnimationFrame(raf);
}
requestAnimationFrame(raf);
// RFC-1175: smoke-test init marker (SMOKE-INIT-01) — set after the last
// addEventListener so component-smoke can synchronize on Lenis readiness.
document.documentElement.setAttribute("data-lenis-initialized", "true");
return activeLenis;
}