export function initProjectsCarousel() { const root = document.querySelector("[data-projects-carousel]"); if (!root) return; const viewport = root.querySelector("[data-projects-viewport]"); const prevBtn = root.parentElement?.querySelector("[data-projects-prev]"); const nextBtn = root.parentElement?.querySelector("[data-projects-next]"); if (!viewport || !prevBtn || !nextBtn) return; const edgePrevBtn = root.querySelector("[data-projects-edge-prev]"); const edgeNextBtn = root.querySelector("[data-projects-edge-next]"); let dragState = null; let suppressClick = false; function syncViewportWidthVar() { const visibleWidth = document.documentElement.clientWidth || window.innerWidth; root.style.setProperty("--projects-viewport-width", `${Math.max(visibleWidth, 0)}px`); } function getEdgeSpace() { const raw = getComputedStyle(root).getPropertyValue("--projects-edge-space"); const value = Number.parseFloat(raw); return Number.isFinite(value) ? Math.max(value, 0) : 0; } function getMinScroll() { return getEdgeSpace(); } function getMaxScroll() { const maxNative = Math.max(viewport.scrollWidth - viewport.clientWidth, 0); return Math.max(maxNative - getEdgeSpace(), getMinScroll()); } function clampScroll(left) { return Math.min(Math.max(left, getMinScroll()), getMaxScroll()); } function getStep() { const firstItem = viewport.querySelector(".projects-carousel__item"); if (!(firstItem instanceof HTMLElement)) return viewport.clientWidth; const track = viewport.querySelector(".projects-carousel__track"); const gapRaw = track ? getComputedStyle(track).columnGap : "0"; const gap = Number.parseFloat(gapRaw) || 0; return firstItem.getBoundingClientRect().width + gap; } function getSnapIndex() { const step = getStep(); if (!step) return 0; const min = getMinScroll(); return Math.round((viewport.scrollLeft - min) / step); } function scrollToIndex(index, behavior = "smooth") { const step = getStep(); const min = getMinScroll(); const max = getMaxScroll(); if (!step) { viewport.scrollTo({ left: clampScroll(viewport.scrollLeft), behavior }); return; } const maxIndex = Math.max(Math.round((max - min) / step), 0); const nextIndex = Math.min(Math.max(index, 0), maxIndex); const target = clampScroll(min + nextIndex * step); viewport.scrollTo({ left: target, behavior }); } function snapToNearest(behavior = "smooth") { scrollToIndex(getSnapIndex(), behavior); } function updateControls() { const x = viewport.scrollLeft; const min = getMinScroll(); const max = getMaxScroll(); prevBtn.disabled = x <= min + 2; nextBtn.disabled = x >= max - 2; } function scrollByStep(direction) { const currentIndex = getSnapIndex(); scrollToIndex(currentIndex + direction, "smooth"); } function getEdgeHitZone() { const sectionInner = root.closest(".section__inner"); const sectionInnerWidth = sectionInner instanceof HTMLElement ? sectionInner.getBoundingClientRect().width : viewport.clientWidth; const viewportWidthWithScrollbar = window.innerWidth || document.documentElement.clientWidth || viewport.clientWidth; const track = viewport.querySelector(".projects-carousel__track"); const gapRaw = track ? getComputedStyle(track).columnGap : "0"; const gap = Number.parseFloat(gapRaw) || 0; const zone = (viewportWidthWithScrollbar - sectionInnerWidth) / 2 - gap; return Math.max(zone, 0); } prevBtn.addEventListener("click", () => scrollByStep(-1)); nextBtn.addEventListener("click", () => scrollByStep(1)); edgePrevBtn?.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); scrollByStep(-1); }); edgeNextBtn?.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); scrollByStep(1); }); viewport.addEventListener("scroll", updateControls, { passive: true }); window.addEventListener("resize", updateControls, { passive: true }); viewport.querySelectorAll("a, img").forEach((el) => { el.setAttribute("draggable", "false"); }); viewport.addEventListener("dragstart", (e) => { e.preventDefault(); }); viewport.addEventListener("pointerdown", (e) => { if (e.button !== 0) return; if (!(e.target instanceof Element)) return; suppressClick = false; dragState = { pointerId: e.pointerId, startX: e.clientX, startScrollLeft: viewport.scrollLeft, moved: false, }; viewport.classList.add("is-dragging"); }); viewport.addEventListener("pointermove", (e) => { if (!dragState || dragState.pointerId !== e.pointerId) return; const dx = e.clientX - dragState.startX; if (Math.abs(dx) > 8) dragState.moved = true; if (dragState.moved) e.preventDefault(); viewport.scrollLeft = clampScroll(dragState.startScrollLeft - dx); }); function finishDrag(e) { if (!dragState || dragState.pointerId !== e.pointerId) return; if (dragState.moved) { e.preventDefault(); suppressClick = true; snapToNearest("smooth"); } else { suppressClick = false; } viewport.classList.remove("is-dragging"); dragState = null; } viewport.addEventListener("pointerup", finishDrag); viewport.addEventListener("pointercancel", finishDrag); viewport.addEventListener("pointerleave", (e) => { if (!dragState) return; finishDrag(e); }); viewport.addEventListener( "click", (e) => { if (suppressClick) { e.preventDefault(); e.stopPropagation(); suppressClick = false; } }, true, ); function alignToStart() { syncViewportWidthVar(); const edgeZone = `${getEdgeHitZone()}px`; root.style.setProperty("--projects-edge-hit-zone", edgeZone); viewport.scrollLeft = getMinScroll(); updateControls(); } alignToStart(); window.addEventListener("load", alignToStart, { once: true }); window.addEventListener( "resize", () => { syncViewportWidthVar(); snapToNearest("auto"); updateControls(); }, { passive: true }, ); }