Build stunning floating parallax section.
1
import {
createContext,
ReactNode,
useCallback,
useContext,
useEffect,
useRef,
} from "react"
import { useAnimationFrame } from "framer-motion"
import { cn } from "@/app/utils/cn"
// import { useMousePosition } from "@/hook/mousePosition"
import { useMousePositionRef } from "@/hook/mousePosition"
interface FloatingContextType {
registerElement: (id: string, element: HTMLDivElement, depth: number) => void
unregisterElement: (id: string) => void
}
const FloatingContext = createContext<FloatingContextType | null>(null)
interface FloatingProps {
children: ReactNode
className?: string
sensitivity?: number
easingFactor?: number
}
const Floating = ({
children,
className,
sensitivity = 1,
easingFactor = 0.05,
...props
}: FloatingProps) => {
const containerRef = useRef<HTMLDivElement>(null)
const elementsMap = useRef(
new Map<
string,
{
element: HTMLDivElement
depth: number
currentPosition: { x: number; y: number }
}
>()
)
const mousePositionRef = useMousePositionRef(containerRef)
const registerElement = useCallback(
(id: string, element: HTMLDivElement, depth: number) => {
elementsMap.current.set(id, {
element,
depth,
currentPosition: { x: 0, y: 0 },
})
},
[]
)
const unregisterElement = useCallback((id: string) => {
elementsMap.current.delete(id)
}, [])
useAnimationFrame(() => {
if (!containerRef.current) return
elementsMap.current.forEach((data) => {
const strength = (data.depth * sensitivity) / 20
// Calculate new target position
const newTargetX = mousePositionRef.current.x * strength
const newTargetY = mousePositionRef.current.y * strength
// Check if we need to update
const dx = newTargetX - data.currentPosition.x
const dy = newTargetY - data.currentPosition.y
// Update position only if we're still moving
data.currentPosition.x += dx * easingFactor
data.currentPosition.y += dy * easingFactor
data.element.style.transform = `translate3d(${data.currentPosition.x}px, ${data.currentPosition.y}px, 0)`
})
})
return (
<FloatingContext.Provider value={{ registerElement, unregisterElement }}>
<div
ref={containerRef}
className={cn("absolute top-0 left-0 w-full h-full", className)}
{...props}
>
{children}
</div>
</FloatingContext.Provider>
)
}
export default Floating
interface FloatingElementProps {
children: ReactNode
className?: string
depth?: number
}
export const FloatingElement = ({
children,
className,
depth = 1,
}: FloatingElementProps) => {
const elementRef = useRef<HTMLDivElement>(null)
const idRef = useRef(Math.random().toString(36).substring(7))
const context = useContext(FloatingContext)
useEffect(() => {
if (!elementRef.current || !context) return
const nonNullDepth = depth ?? 0.01
context.registerElement(idRef.current, elementRef.current, nonNullDepth)
return () => context.unregisterElement(idRef.current)
}, [depth])
return (
<div
ref={elementRef}
className={cn("absolute will-change-transform", className)}
>
{children}
</div>
)
}
2
import { useRef, useEffect } from "react";
export const useMousePositionRef = (
containerRef?: React.RefObject<HTMLElement | SVGElement>
) => {
const mousePositionRef = useRef({ x: 0, y: 0 });
useEffect(() => {
const updatePosition = (x: number, y: number) => {
if (containerRef?.current) {
const rect = containerRef.current.getBoundingClientRect();
mousePositionRef.current = {
x: x - rect.left,
y: y - rect.top,
};
} else {
mousePositionRef.current = { x, y };
}
};
const handleMouseMove = (event: MouseEvent) => {
updatePosition(event.clientX, event.clientY);
};
const handleTouchMove = (event: TouchEvent) => {
const touch = event.touches[0];
updatePosition(touch.clientX, touch.clientY);
};
window.addEventListener("mousemove", handleMouseMove);
window.addEventListener("touchmove", handleTouchMove);
return () => {
window.removeEventListener("mousemove", handleMouseMove);
window.removeEventListener("touchmove", handleTouchMove);
};
}, [containerRef]);
return mousePositionRef;
};
3
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
4
npm install framer-motion