kinetic-slider
Version:
A WebGL-powered kinetic slider component using PIXI.js
824 lines (821 loc) • 26.6 kB
JavaScript
import { jsxs, jsx } from 'react/jsx-runtime';
import { useRef, useState, useEffect, useCallback } from 'react';
import styles from './KineticSlider.module.css.js';
import { Application, Container } from 'pixi.js';
import ResourceManager from './managers/ResourceManager.js';
import { AtlasManager } from './managers/AtlasManager.js';
import { RenderScheduler } from './managers/RenderScheduler.js';
import { UpdateType } from './managers/UpdateTypes.js';
import { ThrottleStrategy } from './managers/FrameThrottler.js';
import { AnimationCoordinator } from './managers/AnimationCoordinator.js';
import SlidingWindowManager from './managers/SlidingWindowManager.js';
import 'pixi-filters';
import './managers/ShaderResourceManager.js';
import './filters/advancedBloomFilter.js';
import './filters/blurFilter.js';
import './filters/colorMatrixFilter.js';
import './filters/dropShadowFilter.js';
import 'gsap';
import './filters/tiltShiftFilter.js';
import { FilterFactory } from './filters/FilterFactory.js';
import { useDisplacementEffects } from './hooks/useDisplacementEffects.js';
import { useFilters } from './hooks/useFilters.js';
import { useSlides } from './hooks/useSlides.js';
import useTextContainers from './hooks/useTextContainers.js';
import useMouseTracking from './hooks/useMouseTracking.js';
import useIdleTimer from './hooks/useIdleTimer.js';
import useNavigation from './hooks/useNavigation.js';
import useExternalNav from './hooks/useExternalNav.js';
import useTouchSwipe from './hooks/useTouchSwipe.js';
import useMouseDrag from './hooks/useMouseDrag.js';
import useTextTilt from './hooks/useTextTilt.js';
import useResizeHandler from './hooks/useResizeHandler.js';
import { loadKineticSliderDependencies } from './ImportHelpers.js';
import { preloadKineticSliderAssets } from './utils/assetPreload.js';
const FILTER_COORDINATION_EVENT = "kinetic-slider:filter-update";
const KineticSlider = ({
// Content sources
images = [],
texts = [],
slidesBasePath = "/images/",
// Displacement settings
backgroundDisplacementSpriteLocation = "/images/background-displace.jpg",
cursorDisplacementSpriteLocation = "/images/cursor-displace.png",
cursorImgEffect = true,
cursorTextEffect = true,
cursorScaleIntensity = 0.65,
cursorMomentum = 0.14,
// Cursor displacement sizing options
cursorDisplacementSizing = "natural",
cursorDisplacementWidth,
cursorDisplacementHeight,
// Text styling
textTitleColor = "white",
textTitleSize = 64,
mobileTextTitleSize = 40,
textTitleLetterspacing = 2,
textTitleFontFamily,
textSubTitleColor = "white",
textSubTitleSize = 24,
mobileTextSubTitleSize = 18,
textSubTitleLetterspacing = 1,
textSubTitleOffsetTop = 10,
mobileTextSubTitleOffsetTop = 5,
textSubTitleFontFamily,
// Animation settings
maxContainerShiftFraction = 0.05,
swipeScaleIntensity = 2,
transitionScaleIntensity = 30,
// Navigation settings
externalNav = false,
navElement = { prev: ".main-nav.prev", next: ".main-nav.next" },
buttonMode = false,
// Filter configurations
imageFilters,
textFilters,
// Atlas configuration
slidesAtlas = "slides-atlas",
effectsAtlas = "effects-atlas",
useEffectsAtlas = false,
useSlidesAtlas = false
}) => {
console.log("KineticSlider received props:", {
useSlidesAtlas,
useEffectsAtlas,
slidesAtlas,
effectsAtlas
});
const sliderRef = useRef(null);
const [isClient, setIsClient] = useState(false);
const [currentSlideIndex, setCurrentSlideIndex] = useState(0);
const [isInteracting, setIsInteracting] = useState(false);
const [isAppReady, setIsAppReady] = useState(false);
const [assetsLoaded, setAssetsLoaded] = useState(false);
const cursorActiveRef = useRef(false);
const scheduler = RenderScheduler.getInstance();
const resourceManagerRef = useRef(null);
const slidingWindowManagerRef = useRef(null);
const animationCoordinator = AnimationCoordinator.getInstance();
useEffect(() => {
if (typeof window === "undefined") return;
scheduler.configureThrottling({
targetFps: 60,
strategy: ThrottleStrategy.PRIORITY
});
return () => {
scheduler.clearQueue();
};
}, [scheduler]);
const atlasManagerRef = useRef(null);
const appRef = useRef(null);
const slidesRef = useRef([]);
const textContainersRef = useRef([]);
const backgroundDisplacementSpriteRef = useRef(null);
const cursorDisplacementSpriteRef = useRef(null);
const bgDispFilterRef = useRef(null);
const cursorDispFilterRef = useRef(null);
const currentIndexRef = useRef(0);
useEffect(() => {
setIsClient(true);
}, []);
useEffect(() => {
const componentId = `kinetic-slider-${Math.random().toString(36).substring(2, 9)}`;
resourceManagerRef.current = new ResourceManager(componentId, {
enableMetrics: true,
enableShaderPooling: true,
logLevel: "warn"
});
FilterFactory.initialize({
enableShaderPooling: true,
enableDebug: false,
lazyLoadConfig: {
unloadTimeoutMs: 12e4,
// 2 minutes
maxCachedModules: 20,
enablePrefetching: true,
retryFailedLoads: true,
maxRetries: 3
}
});
atlasManagerRef.current = new AtlasManager({
debug: true,
preferAtlas: true,
cacheFrameTextures: true,
basePath: "/atlas"
}, resourceManagerRef.current);
if (resourceManagerRef.current) {
animationCoordinator.setResourceManager(resourceManagerRef.current);
}
slidingWindowManagerRef.current = new SlidingWindowManager({
totalSlides: images.length,
initialIndex: 0,
windowSize: 2,
// Default: current slide ±2
debug: false
}, resourceManagerRef.current);
return () => {
if (resourceManagerRef.current) {
console.log("Component unmounting - disposing all resources");
resourceManagerRef.current.markUnmounting();
resourceManagerRef.current.dispose();
resourceManagerRef.current = null;
}
if (atlasManagerRef.current) {
atlasManagerRef.current.dispose();
atlasManagerRef.current = null;
}
slidingWindowManagerRef.current = null;
};
}, [images.length, animationCoordinator]);
const pixiRefs = {
app: appRef,
slides: slidesRef,
textContainers: textContainersRef,
backgroundDisplacementSprite: backgroundDisplacementSpriteRef,
cursorDisplacementSprite: cursorDisplacementSpriteRef,
bgDispFilter: bgDispFilterRef,
cursorDispFilter: cursorDispFilterRef,
currentIndex: currentIndexRef
};
const hookProps = {
images,
texts,
slidesBasePath,
backgroundDisplacementSpriteLocation,
cursorDisplacementSpriteLocation,
cursorImgEffect,
cursorTextEffect,
cursorScaleIntensity,
cursorMomentum,
cursorDisplacementSizing,
cursorDisplacementWidth,
cursorDisplacementHeight,
textTitleColor,
textTitleSize,
mobileTextTitleSize,
textTitleLetterspacing,
textTitleFontFamily,
textSubTitleColor,
textSubTitleSize,
mobileTextSubTitleSize,
textSubTitleLetterspacing,
textSubTitleOffsetTop,
mobileTextSubTitleOffsetTop,
textSubTitleFontFamily,
maxContainerShiftFraction,
swipeScaleIntensity,
transitionScaleIntensity,
imageFilters,
textFilters,
slidesAtlas,
effectsAtlas,
useSlidesAtlas,
useEffectsAtlas
};
const hookParams = {
sliderRef,
pixi: pixiRefs,
props: hookProps,
resourceManager: resourceManagerRef.current,
atlasManager: atlasManagerRef.current,
slidingWindowManager: slidingWindowManagerRef.current
};
const { showDisplacementEffects, hideDisplacementEffects } = useDisplacementEffects({
sliderRef,
bgDispFilterRef,
cursorDispFilterRef,
backgroundDisplacementSpriteRef,
cursorDisplacementSpriteRef,
appRef,
backgroundDisplacementSpriteLocation,
cursorDisplacementSpriteLocation,
cursorImgEffect,
cursorScaleIntensity,
cursorDisplacementSizing,
cursorDisplacementWidth,
cursorDisplacementHeight,
resourceManager: resourceManagerRef.current,
atlasManager: atlasManagerRef.current || void 0,
effectsAtlas,
useEffectsAtlas
});
const {
updateFilterIntensities,
resetAllFilters,
activateFilterEffects,
isInitialized: filtersInitialized,
isActive: filtersActive,
setFiltersActive
} = useFilters(hookParams);
useEffect(() => {
if (!isAppReady || !assetsLoaded) {
console.log("Skipping filter coordination - not ready", {
filtersInitialized,
isAppReady,
assetsLoaded
});
return;
}
if (!filtersInitialized && typeof activateFilterEffects === "function") {
console.log("Attempting to initialize filters during coordination");
activateFilterEffects();
}
console.log(`Filter coordination - isInteracting: ${isInteracting}, filtersActive: ${filtersActive}`);
if (isInteracting) {
console.log("Interaction started - activating effects");
showDisplacementEffects();
if (typeof activateFilterEffects === "function") {
console.log("Using activateFilterEffects");
activateFilterEffects();
} else {
console.log("Using updateFilterIntensities");
updateFilterIntensities(true, true);
}
} else {
console.log("Interaction ended - deactivating effects");
hideDisplacementEffects();
updateFilterIntensities(false);
}
}, [
isInteracting,
isAppReady,
assetsLoaded,
filtersInitialized,
filtersActive,
showDisplacementEffects,
hideDisplacementEffects,
updateFilterIntensities,
activateFilterEffects
]);
useEffect(() => {
if (!isAppReady || !assetsLoaded) {
resetAllFilters();
}
return () => {
resetAllFilters();
};
}, [isAppReady, assetsLoaded, resetAllFilters]);
useEffect(() => {
if (typeof window === "undefined" || !isClient) return;
const loadAssets = async () => {
try {
console.log("Preloading assets and fonts...");
if (atlasManagerRef.current) {
if (slidesAtlas) {
await atlasManagerRef.current.loadAtlas(
slidesAtlas,
`/atlas/${slidesAtlas}.json`,
`/atlas/${slidesAtlas}.png`
);
}
if (effectsAtlas) {
await atlasManagerRef.current.loadAtlas(
effectsAtlas,
`/atlas/${effectsAtlas}.json`,
`/atlas/${effectsAtlas}.png`
);
}
}
await preloadKineticSliderAssets(
images,
backgroundDisplacementSpriteLocation,
cursorDisplacementSpriteLocation,
textTitleFontFamily,
textSubTitleFontFamily
);
setAssetsLoaded(true);
console.log("Assets and fonts preloaded successfully");
} catch (error) {
console.error("Failed to preload assets:", error);
setAssetsLoaded(true);
}
};
loadAssets();
}, [
isClient,
images,
backgroundDisplacementSpriteLocation,
cursorDisplacementSpriteLocation,
textTitleFontFamily,
textSubTitleFontFamily,
slidesAtlas,
effectsAtlas
]);
useEffect(() => {
if (typeof window === "undefined" || !sliderRef.current || appRef.current || !assetsLoaded) return;
const initPixi = async () => {
try {
console.log("Loading PixiJS dependencies...");
const { gsap, pixi, pixiPlugin } = await loadKineticSliderDependencies();
if (typeof window !== "undefined" && pixiPlugin) {
gsap.registerPlugin(pixiPlugin);
if (pixiPlugin.registerPIXI) {
pixiPlugin.registerPIXI(pixi);
}
}
console.log("Creating Pixi.js application...");
const app = new Application();
await app.init({
width: sliderRef.current?.clientWidth || 800,
height: sliderRef.current?.clientHeight || 600,
backgroundAlpha: 0,
resizeTo: sliderRef.current || void 0
});
if (resourceManagerRef.current) {
resourceManagerRef.current.trackPixiApp(app);
}
if (sliderRef.current) {
sliderRef.current.appendChild(app.canvas);
}
appRef.current = app;
const stage = new Container();
app.stage.addChild(stage);
if (resourceManagerRef.current) {
resourceManagerRef.current.trackDisplayObject(stage);
}
setIsAppReady(true);
console.log("Pixi.js application initialized");
} catch (error) {
console.error("Failed to initialize Pixi.js application:", error);
}
};
initPixi();
return () => {
if (appRef.current) {
if (sliderRef.current) {
const canvas = sliderRef.current.querySelector("canvas");
if (canvas) {
sliderRef.current.removeChild(canvas);
}
}
appRef.current = null;
setIsAppReady(false);
}
};
}, [sliderRef.current, assetsLoaded]);
useEffect(() => {
if (typeof window === "undefined" || !isAppReady) return;
if (!hookProps.imageFilters && !hookProps.textFilters) return;
const getFilterTypes = (config) => {
if (!config) return [];
const configs = Array.isArray(config) ? config : [config];
return configs.filter((c) => c && c.type && c.enabled !== false).map((c) => c.type);
};
const imageFilterTypes = getFilterTypes(hookProps.imageFilters);
const textFilterTypes = getFilterTypes(hookProps.textFilters);
const allFilterTypes = [.../* @__PURE__ */ new Set([...imageFilterTypes, ...textFilterTypes])];
if (allFilterTypes.length > 0) {
console.log(`Prefetching ${allFilterTypes.length} filter types:`, allFilterTypes);
FilterFactory.prefetchFilterModules(allFilterTypes, "high");
}
}, [isAppReady, hookProps.imageFilters, hookProps.textFilters]);
const handleSlideChange = useCallback((newIndex) => {
currentIndexRef.current = newIndex;
setCurrentSlideIndex(newIndex);
if (filtersInitialized) {
if (isInteracting) {
console.log("Cursor is within canvas - applying filters immediately with critical priority");
scheduler.scheduleTypedUpdate(
"slider",
UpdateType.DISPLACEMENT_EFFECT,
() => {
showDisplacementEffects();
},
"critical"
);
scheduler.scheduleTypedUpdate(
"slider",
UpdateType.FILTER_UPDATE,
() => {
currentIndexRef.current = newIndex;
if (typeof activateFilterEffects === "function") {
activateFilterEffects();
setFiltersActive(true);
}
},
"critical"
);
} else {
activateFilterEffects();
const transitionDuration = 1e3;
setTimeout(() => {
if (!isInteracting) {
updateFilterIntensities(false);
hideDisplacementEffects();
}
}, transitionDuration);
}
}
}, [
filtersInitialized,
isInteracting,
activateFilterEffects,
updateFilterIntensities,
hideDisplacementEffects,
showDisplacementEffects,
scheduler,
setFiltersActive
]);
const { nextSlide, prevSlide } = useSlides({
...hookParams,
onSlideChange: handleSlideChange
});
useTextContainers({
sliderRef,
appRef,
slidesRef,
textContainersRef,
currentIndex: currentIndexRef,
buttonMode,
texts,
textTitleColor,
textTitleSize,
mobileTextTitleSize,
textTitleLetterspacing,
textTitleFontFamily,
textSubTitleColor,
textSubTitleSize,
mobileTextSubTitleSize,
textSubTitleLetterspacing,
textSubTitleOffsetTop,
mobileTextSubTitleOffsetTop,
textSubTitleFontFamily,
resourceManager: resourceManagerRef.current
});
useMouseTracking({
...hookParams,
backgroundDisplacementSpriteRef,
cursorDisplacementSpriteRef,
cursorImgEffect,
cursorMomentum
});
useIdleTimer({
sliderRef,
cursorActive: cursorActiveRef,
bgDispFilterRef,
cursorDispFilterRef,
cursorImgEffect,
defaultBgFilterScale: 20,
defaultCursorFilterScale: 10,
resourceManager: resourceManagerRef.current
});
const handleNext = useCallback(() => {
if (!appRef.current || !isAppReady || slidesRef.current.length === 0) return;
const nextIndex = (currentSlideIndex + 1) % slidesRef.current.length;
scheduler.scheduleTypedUpdate(
"slider",
UpdateType.SLIDE_TRANSITION,
() => {
nextSlide(nextIndex);
currentIndexRef.current = nextIndex;
setCurrentSlideIndex(nextIndex);
setTimeout(() => {
console.log("Scheduling effects after slide change (next)");
scheduler.scheduleTypedUpdate(
"slider",
UpdateType.DISPLACEMENT_EFFECT,
() => {
showDisplacementEffects();
},
// Use critical priority if cursor is within the canvas
isInteracting ? "critical" : void 0
);
scheduler.scheduleTypedUpdate(
"slider",
UpdateType.FILTER_UPDATE,
() => {
currentIndexRef.current = nextIndex;
if (typeof activateFilterEffects === "function") {
activateFilterEffects();
if (isInteracting) {
setFiltersActive(true);
}
}
},
// Use critical priority if cursor is within the canvas
isInteracting ? "critical" : void 0
);
}, 100);
}
);
}, [
appRef,
isAppReady,
slidesRef,
currentSlideIndex,
nextSlide,
isInteracting,
showDisplacementEffects,
activateFilterEffects,
scheduler,
setFiltersActive
]);
const handlePrev = useCallback(() => {
if (!appRef.current || !isAppReady || slidesRef.current.length === 0) return;
const prevIndex = (currentSlideIndex - 1 + slidesRef.current.length) % slidesRef.current.length;
scheduler.scheduleTypedUpdate(
"slider",
UpdateType.SLIDE_TRANSITION,
() => {
prevSlide(prevIndex);
currentIndexRef.current = prevIndex;
setCurrentSlideIndex(prevIndex);
setTimeout(() => {
console.log("Scheduling effects after slide change (prev)");
scheduler.scheduleTypedUpdate(
"slider",
UpdateType.DISPLACEMENT_EFFECT,
() => {
showDisplacementEffects();
},
// Use critical priority if cursor is within the canvas
isInteracting ? "critical" : void 0
);
scheduler.scheduleTypedUpdate(
"slider",
UpdateType.FILTER_UPDATE,
() => {
currentIndexRef.current = prevIndex;
if (typeof activateFilterEffects === "function") {
activateFilterEffects();
if (isInteracting) {
setFiltersActive(true);
}
}
},
// Use critical priority if cursor is within the canvas
isInteracting ? "critical" : void 0
);
}, 100);
}
);
}, [
appRef,
isAppReady,
slidesRef,
currentSlideIndex,
prevSlide,
isInteracting,
showDisplacementEffects,
activateFilterEffects,
scheduler,
setFiltersActive
]);
useEffect(() => {
if (!appRef.current || !isAppReady) return;
currentIndexRef.current = currentSlideIndex;
}, [appRef.current, currentSlideIndex, isAppReady]);
useNavigation({
onNext: handleNext,
onPrev: handlePrev,
enableKeyboardNav: true
});
useExternalNav({
externalNav,
navElement,
handleNext,
handlePrev
});
useTouchSwipe({
sliderRef,
onSwipeLeft: handleNext,
onSwipeRight: handlePrev
});
useMouseDrag({
sliderRef,
slidesRef,
currentIndex: currentIndexRef,
swipeScaleIntensity,
swipeDistance: typeof window !== "undefined" ? window.innerWidth * 0.2 : 200,
onSwipeLeft: handleNext,
onSwipeRight: handlePrev
});
useTextTilt({
sliderRef,
textContainersRef,
currentIndex: currentIndexRef,
cursorTextEffect,
maxContainerShiftFraction,
bgDispFilterRef,
cursorDispFilterRef,
cursorImgEffect
});
useResizeHandler({
sliderRef,
appRef,
slidesRef,
textContainersRef,
backgroundDisplacementSpriteRef,
cursorDisplacementSpriteRef
});
const handleMouseEnter = useCallback(() => {
if (!isAppReady) return;
console.log("Mouse entered the slider - activating effects immediately", {
filtersInitialized,
filtersActive,
hasActivateFunction: typeof activateFilterEffects === "function",
hasUpdateFunction: typeof updateFilterIntensities === "function",
currentSlideIndex
});
cursorActiveRef.current = true;
setIsInteracting(true);
scheduler.scheduleTypedUpdate(
"slider",
UpdateType.DISPLACEMENT_EFFECT,
() => {
showDisplacementEffects();
console.log("Displacement effects activated");
},
"critical"
);
scheduler.scheduleTypedUpdate(
"slider",
UpdateType.FILTER_UPDATE,
() => {
if (!filtersInitialized && typeof activateFilterEffects === "function") {
console.log("Filters not initialized, initializing now");
activateFilterEffects();
} else if (typeof activateFilterEffects === "function") {
console.log("Using activateFilterEffects function for slide", currentSlideIndex);
activateFilterEffects();
} else if (typeof updateFilterIntensities === "function") {
console.log("Using updateFilterIntensities function with force=true");
updateFilterIntensities(true, true);
}
setFiltersActive(true);
console.log("Filter activation completed with critical priority");
},
"critical"
);
scheduler.scheduleTypedUpdate(
"slider",
UpdateType.FILTER_UPDATE,
() => {
console.log("Final render update after mouse enter completed");
console.log("Filter state after render:", {
filtersActive,
bgDispFilterEnabled: bgDispFilterRef.current?.enabled,
cursorDispFilterEnabled: cursorDispFilterRef.current?.enabled,
currentSlideIndex
});
},
"critical"
);
}, [
isAppReady,
showDisplacementEffects,
updateFilterIntensities,
activateFilterEffects,
filtersInitialized,
filtersActive,
setFiltersActive,
currentSlideIndex,
scheduler
]);
const handleMouseLeave = useCallback(() => {
if (!isAppReady) return;
setIsInteracting(false);
const event1 = new CustomEvent(FILTER_COORDINATION_EVENT, {
detail: {
type: "background-displacement",
intensity: 0,
timestamp: Date.now(),
source: "slider-component",
priority: "critical"
}
});
window.dispatchEvent(event1);
const event2 = new CustomEvent(FILTER_COORDINATION_EVENT, {
detail: {
type: "cursor-displacement",
intensity: 0,
timestamp: Date.now(),
source: "slider-component",
priority: "critical"
}
});
window.dispatchEvent(event2);
updateFilterIntensities(false, true);
scheduler.scheduleTypedUpdate(
"slider",
UpdateType.FILTER_UPDATE,
() => {
},
"critical"
);
}, [isAppReady, setIsInteracting, updateFilterIntensities, scheduler]);
useEffect(() => {
return () => {
resetAllFilters();
if (resourceManagerRef.current) {
resourceManagerRef.current.clearPendingUpdates();
}
const transitionTimeouts = Array.from(document.querySelectorAll(`[data-slider-id="${sliderRef.current?.id}"]`)).map((el) => parseInt(el.getAttribute("data-timeout-id") || "0")).filter((id) => id > 0);
transitionTimeouts.forEach(clearTimeout);
};
}, [resetAllFilters]);
useEffect(() => {
const handleError = (error) => {
console.error("Filter system error:", error);
resetAllFilters();
hideDisplacementEffects();
};
window.addEventListener("error", (e) => handleError(e.error));
return () => window.removeEventListener("error", (e) => handleError(e.error));
}, [resetAllFilters, hideDisplacementEffects]);
useEffect(() => {
return () => {
if (resourceManagerRef.current) {
resourceManagerRef.current.clearPendingUpdates();
resourceManagerRef.current.markUnmounting();
}
};
}, []);
useEffect(() => {
if (!resourceManagerRef.current) return;
let frameCount = 0;
let lastTime = performance.now();
let fps = 60;
const monitorPerformance = () => {
frameCount++;
const currentTime = performance.now();
const elapsed = currentTime - lastTime;
if (elapsed > 1e3) {
fps = frameCount * 1e3 / elapsed;
frameCount = 0;
lastTime = currentTime;
if (resourceManagerRef.current && fps < 55) {
resourceManagerRef.current.autoOptimizeFilters(fps, 55);
}
}
performanceMonitorId = requestAnimationFrame(monitorPerformance);
};
let performanceMonitorId = requestAnimationFrame(monitorPerformance);
return () => {
if (performanceMonitorId) {
cancelAnimationFrame(performanceMonitorId);
}
};
}, [resourceManagerRef]);
return /* @__PURE__ */ jsxs(
"div",
{
className: styles.kineticSlider,
ref: sliderRef,
onMouseEnter: handleMouseEnter,
onMouseLeave: handleMouseLeave,
children: [
(!isAppReady || !assetsLoaded) && /* @__PURE__ */ jsx("div", { className: styles.placeholder, children: /* @__PURE__ */ jsxs("div", { className: styles.loadingIndicator, children: [
/* @__PURE__ */ jsx("div", { className: styles.spinner }),
/* @__PURE__ */ jsx("div", { children: "Loading slider..." })
] }) }),
!externalNav && isClient && /* @__PURE__ */ jsxs("nav", { children: [
/* @__PURE__ */ jsx("button", { onClick: handlePrev, className: styles.prev, children: "Prev" }),
/* @__PURE__ */ jsx("button", { onClick: handleNext, className: styles.next, children: "Next" })
] })
]
}
);
};
export { KineticSlider as default };
//# sourceMappingURL=KineticSlider.js.map