lightswind
Version:
A professionally designed animate react component library & templates market that brings together functionality, accessibility, and beautiful aesthetics for modern applications.
194 lines (193 loc) • 9.35 kB
JavaScript
import { jsx as _jsx } from "react/jsx-runtime";
import { useRef, useEffect, useState } from "react";
const cn = (...classes) => classes.filter(Boolean).join(" ");
const ThreeDHoverGallery = ({
images = [
"https://images.pexels.com/photos/26797335/pexels-photo-26797335/free-photo-of-scenic-view-of-mountains.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2",
"https://images.pexels.com/photos/12194487/pexels-photo-12194487.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2",
"https://images.pexels.com/photos/32423809/pexels-photo-32423809/free-photo-of-aerial-view-of-kayaking-at-robberg-south-africa.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2",
"https://images.pexels.com/photos/32296519/pexels-photo-32296519/free-photo-of-rocky-coastline-of-cape-point-with-turquoise-waters.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2",
"https://images.pexels.com/photos/32396739/pexels-photo-32396739/free-photo-of-serene-motorcycle-ride-through-bamboo-grove.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2",
"https://images.pexels.com/photos/32304900/pexels-photo-32304900/free-photo-of-scenic-view-of-cape-town-s-twelve-apostles.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2",
"https://images.pexels.com/photos/32437034/pexels-photo-32437034/free-photo-of-fisherman-holding-freshly-caught-red-drum-fish.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2",
"https://images.pexels.com/photos/32469847/pexels-photo-32469847/free-photo-of-deer-drinking-from-natural-water-source-in-wilderness.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2",
],
itemWidth = 12, // Increased default for more width
itemHeight = 20, // Increased default for more height
gap = 1.2, // Increased default for more spacing between items
perspective = 50, // Increased default for a stronger 3D effect
hoverScale = 15, // Increased default for more pronounced hover scale
transitionDuration = 1.25,
backgroundColor,
grayscaleStrength = 1,
brightnessLevel = 0.5,
activeWidth = 45, // Increased default for wider active item
rotationAngle = 35,
zDepth = 10, // Increased default for deeper Z-axis effect
enableKeyboardNavigation = true,
autoPlay = false,
autoPlayDelay = 3000,
className,
style,
onImageClick,
onImageHover,
onImageFocus,
}) => {
const containerRef = useRef(null);
const [activeIndex, setActiveIndex] = useState(null);
const [focusedIndex, setFocusedIndex] = useState(null);
const autoPlayRef = useRef(null);
// Effect for auto-play functionality
useEffect(() => {
if (autoPlay && images.length > 0) {
// Clear any existing interval to prevent multiple intervals running
if (autoPlayRef.current) {
clearInterval(autoPlayRef.current);
}
autoPlayRef.current = setInterval(() => {
setActiveIndex((prev) => {
// Calculate the next index, looping back to the start if at the end
const nextIndex = prev === null ? 0 : (prev + 1) % images.length;
return nextIndex;
});
}, autoPlayDelay);
// Cleanup function to clear the interval when the component unmounts or dependencies change
return () => {
if (autoPlayRef.current) {
clearInterval(autoPlayRef.current);
}
};
}
// If autoPlay is false or no images, ensure interval is cleared
if (!autoPlay && autoPlayRef.current) {
clearInterval(autoPlayRef.current);
autoPlayRef.current = null;
}
}, [autoPlay, autoPlayDelay, images.length]); // Dependencies for the effect
// Handler for image click event
const handleImageClick = (index, image) => {
// Toggle active state: if clicked item is already active, deactivate it
setActiveIndex(activeIndex === index ? null : index);
onImageClick?.(index, image); // Call optional onImageClick prop
};
// Handler for image hover (mouse enter) event
const handleImageHover = (index, image) => {
// Only set active index on hover if autoPlay is not enabled
if (!autoPlay) {
setActiveIndex(index);
}
onImageHover?.(index, image); // Call optional onImageHover prop
};
// Handler for image leave (mouse leave) event
const handleImageLeave = () => {
// Only clear active index on leave if autoPlay is not enabled
if (!autoPlay) {
setActiveIndex(null);
}
};
// Handler for image focus event (e.g., via keyboard navigation)
const handleImageFocus = (index, image) => {
setFocusedIndex(index); // Set the focused index
onImageFocus?.(index, image); // Call optional onImageFocus prop
};
// Handler for keyboard navigation
const handleKeyDown = (event, index) => {
if (!enableKeyboardNavigation) return; // Exit if keyboard navigation is disabled
switch (event.key) {
case "Enter":
case " ": // Space key
event.preventDefault(); // Prevent default scroll behavior for space key
handleImageClick(index, images[index]); // Simulate click
break;
case "ArrowLeft":
event.preventDefault(); // Prevent default scroll behavior
// Calculate previous index, looping to the end if at the beginning
const prevIndex = index > 0 ? index - 1 : images.length - 1;
// Focus the previous element if it exists
containerRef.current?.children[prevIndex]?.focus();
break;
case "ArrowRight":
event.preventDefault(); // Prevent default scroll behavior
// Calculate next index, looping to the start if at the end
const nextIndex = index < images.length - 1 ? index + 1 : 0;
// Focus the next element if it exists
containerRef.current?.children[nextIndex]?.focus();
break;
}
};
// Function to determine the style for each gallery item
const getItemStyle = (index) => {
const isActive = activeIndex === index;
const isFocused = focusedIndex === index;
// A small base width to ensure items are always visible, especially on very small screens
const baseWidthPx = 10;
return {
// Width calculation: active item gets activeWidth, others get itemWidth + a base pixel width
width: isActive
? `${activeWidth}vw`
: `calc(${itemWidth}vw + ${baseWidthPx}px)`,
// Height calculation: uses a combination of viewport width and height units for responsiveness
height: `calc(${itemHeight}vw + ${itemHeight}vh)`,
backgroundImage: `url(${images[index]})`, // Set background image
backgroundSize: "cover", // Cover the entire area
backgroundPosition: "center", // Center the image
backgroundColor, // Fallback background color
cursor: "pointer", // Indicate interactivity
// Apply grayscale and brightness filters if not active or focused
filter:
isActive || isFocused
? "inherit"
: `grayscale(${grayscaleStrength}) brightness(${brightnessLevel})`,
// Apply 3D transform for active item, and transitions for smooth animation
transform: isActive
? `translateZ(calc(${hoverScale}vw + ${hoverScale}vh))`
: "none",
transition: `transform ${transitionDuration}s cubic-bezier(.1, .7, 0, 1), filter 3s cubic-bezier(.1, .7, 0, 1), width ${transitionDuration}s cubic-bezier(.1, .7, 0, 1)`,
willChange: "transform, filter, width", // Optimize for animation performance
zIndex: isActive ? 100 : "auto", // Bring active item to front
margin: isActive ? "0 0.45vw" : "0", // Add slight margin for active item
outline: isFocused ? "2px solid #3b82f6" : "none", // Outline for focused item
outlineOffset: "2px", // Offset for the outline
borderRadius: "0.5rem", // Apply rounded corners to items
};
};
return _jsx("div", {
className: cn(
"flex items-center justify-center min-h-screen w-full overflow-hidden bg-background",
className
),
style: backgroundColor ? { backgroundColor, ...style } : style,
children: _jsx("div", {
ref: containerRef,
// Inner flex container for the images, centered and taking full width.
// Applies the 3D perspective and gap between items.
className: "flex justify-center items-center w-full",
style: {
perspective: `calc(${perspective}vw + ${perspective}vh)`,
gap: `${gap}rem`,
},
children: images.map((image, index) =>
_jsx(
"div",
{
// Individual image item, applies styling, interactivity, and accessibility attributes.
className: "relative will-change-transform rounded-lg shadow-lg", // Added Tailwind classes for rounded corners and shadow
style: getItemStyle(index),
tabIndex: enableKeyboardNavigation ? 0 : -1,
onClick: () => handleImageClick(index, image),
onMouseEnter: () => handleImageHover(index, image),
onMouseLeave: handleImageLeave,
onFocus: () => handleImageFocus(index, image),
onBlur: () => setFocusedIndex(null),
onKeyDown: (e) => handleKeyDown(e, index),
role: "button", // Indicate that this div acts as a button
"aria-label": `Image ${index + 1} of ${images.length}`,
"aria-pressed": activeIndex === index,
},
index
)
),
}),
});
};
export default ThreeDHoverGallery;