(0);
// useScroll hook from Framer Motion to track scroll progress (can be used for additional animations)
const { scrollYProgress } = useScroll({ container: listRef });
useEffect(() => {
const updateFocusedItem = () => {
if (!listRef.current) return;
const container = listRef.current;
// Get all direct children (the motion.div items)
const children = Array.from(container.children) as HTMLDivElement[];
const scrollTop = container.scrollTop; // Current vertical scroll position
const containerCenter = container.clientHeight / 2; // Vertical center of the container
let closestItemIndex = 0;
let minDistanceToCenter = Infinity; // Initialize with a very large number
// Iterate over each child item to find the one closest to the center
children.forEach((child, index) => {
const itemTop = child.offsetTop; // Top position of the item relative to its parent
const actualItemHeight = child.offsetHeight; // Actual rendered height of the item
const itemCenter = itemTop + actualItemHeight / 2; // Vertical center of the item
// Calculate the distance from the item's center to the container's center, adjusted for scroll
const distanceToCenter = Math.abs(
itemCenter - scrollTop - containerCenter
);
// If this item is closer to the center than the previous closest
if (distanceToCenter < minDistanceToCenter) {
minDistanceToCenter = distanceToCenter;
closestItemIndex = index;
}
});
// Update the focused index state
setFocusedIndex(closestItemIndex);
};
// Call immediately on mount to set initial focused item
updateFocusedItem();
// Add scroll event listener to update focused item on scroll
const listElement = listRef.current;
if (listElement) {
listElement.addEventListener("scroll", updateFocusedItem);
}
// Cleanup function: remove the event listener when the component unmounts
return () => {
if (listElement) {
listElement.removeEventListener("scroll", updateFocusedItem);
}
};
}, [data, itemHeight]); // Dependencies: Re-run effect if data or itemHeight changes
// Framer Motion Variants for defining animation states for each item
const itemVariants: Variants = {
hidden: {
opacity: 0,
scale: 0.7,
transition: { duration: 0.35, ease: "easeOut" },
},
focused: {
opacity: 1,
scale: 1,
zIndex: 10,
transition: { duration: 0.35, ease: "easeOut" },
},
next: {
opacity: 1,
scale: 0.95,
zIndex: 5,
transition: { duration: 0.35, ease: "easeOut" },
},
visible: {
opacity: 1,
scale: 1,
transition: { duration: 0.35, ease: "easeOut" },
},
};
return (
{data.map((item, index) => {
let variant = "hidden"; // Default variant
// Determine the animation variant based on the item's position relative to the focused item
if (index === focusedIndex) {
variant = "focused"; // The currently focused item
} else if (index === focusedIndex + 1) {
variant = "next"; // The item immediately following the focused one
} else {
// Items within a certain range (2 items above/below) of the focused item are visible
const isWithinVisibleRange = Math.abs(index - focusedIndex) <= 2;
if (isWithinVisibleRange) {
variant = "visible";
}
}
return (
{renderItem(item, index)}
);
})}
);
};
export default ScrollList;
```