// Interactive, hydration-safe content carousel. component Carousel { outputs { initialize(payload: { index: number; count: number }) change(payload: { index: number; previousIndex: number; item: string | number | boolean | null | object; reason: string | boolean }) previous(payload: { index: number; previousIndex: number; item: string | number | boolean | null | object }) next(payload: { index: number; previousIndex: number; item: string | number | boolean | null | object }) play(payload: { index: number; interval: number }) pause(payload: { index: number }) reachStart(payload: { index: number }) reachEnd(payload: { index: number }) dragStart(payload: { index: number; x: null }) dragEnd(payload: { index: number; distance: number }) } props { size: string = "default" color: string = "primary" title: string = "" description: string = "" items: unknown[] = [] activeIndex: number = 0 slidesPerView: number = 1 gap: string = "0.75rem" showPagination: boolean = false isAutoPlay: boolean = false autoplayInterval: number = 4000 isInfiniteLoop: boolean = false isRTL: boolean = false isCentered: boolean = false isDraggable: boolean = false isAutoHeight: boolean = false isSnap: boolean = false showCounter: boolean = false thumbnails: string = "none" ariaLabel: string = "Content carousel" variant: string = "default" class: string = "" } state currentIndex = activeIndex state playing: boolean = false state dragOrigin = null state dragOffset: number = 0 state autoplayTimer = null state carouselRoot = null functions { shared function slideCount() { return items.length } shared function maximumIndex(count, visible) { count = slideCount() visible = Math.max(1, Number(slidesPerView) || 1) if (isCentered || isSnap) { return Math.max(0, count - 1) } return Math.max(0, count - visible) } shared function normalizedIndex(index, maximum) { maximum = maximumIndex() if (slideCount() === 0) { return 0 } if (isInfiniteLoop) { if (index < 0) { return maximum } if (index > maximum) { return 0 } } return Math.max(0, Math.min(index, maximum)) } client function rememberRoot(sourceEvent, root) { if (!sourceEvent || !sourceEvent.currentTarget) { return } root = sourceEvent.currentTarget.closest(".wire-next--carousel") if (root) { carouselRoot = root } } client function syncNavigation(sourceEvent, index, root, viewport, slides, slide, thumbnails, thumbnail, rail) { rememberRoot(sourceEvent) root = carouselRoot if (!root) { return } if (isSnap) { viewport = root.querySelector(".wire-next__carousel-viewport") slides = root.querySelectorAll(".wire-next__carousel-slide") slide = slides[index] if (viewport && slide) { viewport.scrollTo({ left: slide.offsetLeft - (viewport.clientWidth - slide.clientWidth) / 2, behavior: "smooth" }) } } thumbnails = root.querySelectorAll(".wire-next__carousel-thumbnails button") if (thumbnails[index]) { thumbnail = thumbnails[index] rail = thumbnail.parentElement rail.scrollTo({ left: thumbnail.offsetLeft - (rail.clientWidth - thumbnail.clientWidth) / 2, top: thumbnail.offsetTop - (rail.clientHeight - thumbnail.clientHeight) / 2, behavior: "smooth" }) } } client function handleSnapScroll(sourceEvent, slides, viewport, viewportCenter, closestIndex, closestDistance, slide, slideCenter, distance, previousIndex) { if (!isSnap) { return } rememberRoot(sourceEvent) viewport = sourceEvent.currentTarget slides = viewport.querySelectorAll(".wire-next__carousel-slide") viewportCenter = viewport.scrollLeft + viewport.clientWidth / 2 closestIndex = currentIndex closestDistance = 1000000000 slides.forEach(function (candidate, index) { slideCenter = candidate.offsetLeft + candidate.clientWidth / 2 distance = Math.abs(slideCenter - viewportCenter) if (distance < closestDistance) { closestDistance = distance closestIndex = index } }) closestIndex = normalizedIndex(closestIndex) if (closestIndex !== currentIndex) { previousIndex = currentIndex currentIndex = closestIndex output.change({ index: currentIndex, previousIndex: previousIndex, item: items[currentIndex], reason: "snap" }) slides = carouselRoot.querySelectorAll(".wire-next__carousel-thumbnails button") slide = slides[currentIndex] if (slide) { viewport = slide.parentElement viewport.scrollTo({ left: slide.offsetLeft - (viewport.clientWidth - slide.clientWidth) / 2, top: slide.offsetTop - (viewport.clientHeight - slide.clientHeight) / 2, behavior: "smooth" }) } } } client function selectSlide(index, reason, sourceEvent, previousIndex) { if (slideCount() === 0) { return } previousIndex = currentIndex currentIndex = normalizedIndex(Number(index)) syncNavigation(sourceEvent, currentIndex) if (previousIndex === currentIndex && reason !== "initialize") { return } output.change({ index: currentIndex, previousIndex: previousIndex, item: items[currentIndex], reason: reason || "select" }) if (currentIndex === 0) { output.reachStart({ index: currentIndex }) } if (currentIndex === maximumIndex()) { output.reachEnd({ index: currentIndex }) } } client function previousSlide(sourceEvent, previousIndex) { previousIndex = currentIndex selectSlide(currentIndex - 1, "previous", sourceEvent) output.previous({ index: currentIndex, previousIndex: previousIndex, item: items[currentIndex] }) } client function nextSlide(sourceEvent, previousIndex) { previousIndex = currentIndex selectSlide(currentIndex + 1, "next", sourceEvent) output.next({ index: currentIndex, previousIndex: previousIndex, item: items[currentIndex] }) } client function handleKeydown(sourceEvent) { if (sourceEvent.key === "ArrowLeft") { sourceEvent.preventDefault() if (isRTL) { nextSlide(sourceEvent) } else { previousSlide(sourceEvent) } } if (sourceEvent.key === "ArrowRight") { sourceEvent.preventDefault() if (isRTL) { previousSlide(sourceEvent) } else { nextSlide(sourceEvent) } } if (sourceEvent.key === "Home") { sourceEvent.preventDefault() selectSlide(0, "keyboard", sourceEvent) } if (sourceEvent.key === "End") { sourceEvent.preventDefault() selectSlide(maximumIndex(), "keyboard", sourceEvent) } } client function beginDrag(sourceEvent) { if (!isDraggable || isSnap) { return } sourceEvent.preventDefault() dragOrigin = sourceEvent.clientX dragOffset = 0 sourceEvent.currentTarget.setPointerCapture(sourceEvent.pointerId) output.dragStart({ index: currentIndex, x: dragOrigin }) } client function moveDrag(sourceEvent) { if (!isDraggable || isSnap || dragOrigin === null) { return } sourceEvent.preventDefault() dragOffset = sourceEvent.clientX - dragOrigin } shared function cancelDrag() { dragOrigin = null dragOffset = 0 } client function endDrag(sourceEvent, distance) { if (!isDraggable || isSnap || dragOrigin === null) { return } sourceEvent.preventDefault() distance = dragOffset || sourceEvent.clientX - dragOrigin dragOrigin = null dragOffset = 0 if (Math.abs(distance) > 20) { if ((distance < 0 && !isRTL) || (distance > 0 && isRTL)) { nextSlide(sourceEvent) } else { previousSlide(sourceEvent) } } output.dragEnd({ index: currentIndex, distance: distance }) } shared function advanceAutoplay() { if (currentIndex >= maximumIndex()) { selectSlide(0, "autoplay") } else { selectSlide(currentIndex + 1, "autoplay") } } client function startAutoplay() { if (!isAutoPlay || playing || slideCount() < 2) { return } playing = true autoplayTimer = setInterval(advanceAutoplay, Math.max(1000, Number(autoplayInterval))) output.play({ index: currentIndex, interval: autoplayInterval }) } client function pauseAutoplay() { if (autoplayTimer) { clearInterval(autoplayTimer) } autoplayTimer = null if (playing) { output.pause({ index: currentIndex }) } playing = false } } lifecycle { mount { output.initialize({ index: currentIndex, count: slideCount() }) startAutoplay() } unmount { if (autoplayTimer) { clearInterval(autoplayTimer) } } } view { } }