diff --git a/app/stickers/page.tsx b/app/stickers/page.tsx
new file mode 100644
index 0000000..cf72172
--- /dev/null
+++ b/app/stickers/page.tsx
@@ -0,0 +1,74 @@
+"use client"
+
+import Floating, { FloatingElement } from "../../components/fancy/parallax-floating"
+import LandingLayoutView from "@hhs/layouts/landing-layout"
+import { motion } from "framer-motion"
+import { useRef } from "react"
+
+const bgColors = [
+ "bg-red-500",
+ "bg-blue-500",
+ "bg-green-500",
+ "bg-yellow-500",
+ "bg-purple-500",
+ "bg-pink-500",
+ "bg-indigo-500",
+ "bg-orange-500",
+]
+
+const positions = [
+ "top-1/4 left-[10%]",
+ "top-1/2 left-[15%]",
+ "top-1/3 left-[40%]",
+ "top-2/3 left-[60%]",
+ "top-1/4 right-[20%]",
+ "bottom-1/3 left-[20%]",
+ "bottom-1/2 right-[30%]",
+ "bottom-1/4 left-[45%]",
+]
+
+const stickers = Array.from({ length: 8 }, (_, i) => ({
+ depth: Math.random() * 2 + 0.5,
+ className: `${positions[i]} ${bgColors[i]} w-32 h-32 rounded-xl cursor-pointer`,
+}))
+
+export default function StickersPage() {
+ const scope = useRef(null)
+
+ return (
+
+
+
+ {stickers.map((sticker, index) => (
+
+
+
+ ))}
+
+
+
+
+ )
+}
diff --git a/bun.lockb b/bun.lockb
index 4b8d6b8..19bb597 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/components/fancy/parallax-floating.tsx b/components/fancy/parallax-floating.tsx
new file mode 100644
index 0000000..c04af5c
--- /dev/null
+++ b/components/fancy/parallax-floating.tsx
@@ -0,0 +1,134 @@
+"use client"
+
+import {
+ createContext,
+ ReactNode,
+ useCallback,
+ useContext,
+ useEffect,
+ useRef,
+} from "react"
+import { useAnimationFrame } from "motion/react"
+
+import { cn } from "@hhs/utils/cn"
+import { useMousePositionRef } from "@hhs/hooks/use-mouse-position-ref"
+
+interface FloatingContextType {
+ registerElement: (id: string, element: HTMLDivElement, depth: number) => void
+ unregisterElement: (id: string) => void
+}
+
+const FloatingContext = createContext(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(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 (
+
+
+ {children}
+
+
+ )
+}
+
+export default Floating
+
+interface FloatingElementProps {
+ children: ReactNode
+ className?: string
+ depth?: number
+}
+
+export const FloatingElement = ({
+ children,
+ className,
+ depth = 1,
+}: FloatingElementProps) => {
+ const elementRef = useRef(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 (
+
+ {children}
+
+ )
+}
diff --git a/constants/layout.ts b/constants/layout.ts
index e8376d7..77f5758 100644
--- a/constants/layout.ts
+++ b/constants/layout.ts
@@ -27,6 +27,10 @@ export const NAV_ITEMS = [
label: "Live",
href: "/live",
},
+ {
+ label: "Stickers",
+ href: "/stickers",
+ },
{
label: "HHS",
href: "#",
diff --git a/hooks/use-mouse-position-ref.ts b/hooks/use-mouse-position-ref.ts
new file mode 100644
index 0000000..c801d82
--- /dev/null
+++ b/hooks/use-mouse-position-ref.ts
@@ -0,0 +1,42 @@
+import { RefObject, useEffect, useRef } from "react"
+
+export const useMousePositionRef = (
+ containerRef?: RefObject
+) => {
+ const positionRef = useRef({ x: 0, y: 0 })
+
+ useEffect(() => {
+ const updatePosition = (x: number, y: number) => {
+ if (containerRef && containerRef.current) {
+ const rect = containerRef.current.getBoundingClientRect()
+ const relativeX = x - rect.left
+ const relativeY = y - rect.top
+
+ // Calculate relative position even when outside the container
+ positionRef.current = { x: relativeX, y: relativeY }
+ } else {
+ positionRef.current = { x, y }
+ }
+ }
+
+ const handleMouseMove = (ev: MouseEvent) => {
+ updatePosition(ev.clientX, ev.clientY)
+ }
+
+ const handleTouchMove = (ev: TouchEvent) => {
+ const touch = ev.touches[0]
+ updatePosition(touch.clientX, touch.clientY)
+ }
+
+ // Listen for both mouse and touch events
+ window.addEventListener("mousemove", handleMouseMove)
+ window.addEventListener("touchmove", handleTouchMove)
+
+ return () => {
+ window.removeEventListener("mousemove", handleMouseMove)
+ window.removeEventListener("touchmove", handleTouchMove)
+ }
+ }, [containerRef])
+
+ return positionRef
+}
diff --git a/package.json b/package.json
index 3cdecc2..b5dc833 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,7 @@
"ical-generator": "^8.0.1",
"lodash.debounce": "^4.0.8",
"lucide-react": "^0.447.0",
+ "motion": "^11.18.0",
"next": "14.2.21",
"next-themes": "^0.3.0",
"react": "^18",