'use client' import * as Scrollytelling from '@bsmnt/scrollytelling' import { useRef, useState, useEffect } from 'react' import { motion, AnimatePresence, useInView } from 'framer-motion' import { Dots, Stripes } from '../Visuals' import { cn } from 'ui' const titles = [ 'Thank you to our community.', 'You created more Supabase databases in 2025 than in all previous years combined.', "You've stored over 3.9B objects, and called almost 50B edge functions.", "Together, we've launched new ideas, startups, community projects, weekend hackathons…", 'Thank you for building with us.', ] const MIN_DISTANCE = 12 // Minimum distance between box centers (in percentage) type FloatingBox = { id: number type: 'dots' | 'stripes' top: number left: number size: string drift: { x: number; y: number } } function getDistance(box1: { top: number; left: number }, box2: { top: number; left: number }) { return Math.sqrt(Math.pow(box1.top - box2.top, 2) + Math.pow(box1.left - box2.left, 2)) } function generateNonOverlappingPosition( existingBoxes: FloatingBox[] ): { top: number; left: number } | null { const maxAttempts = 20 for (let attempt = 0; attempt < maxAttempts; attempt++) { const top = Math.random() * 80 + 5 const left = Math.random() * 80 + 5 const hasOverlap = existingBoxes.some( (box) => getDistance({ top, left }, { top: box.top, left: box.left }) < MIN_DISTANCE ) if (!hasOverlap) { return { top, left } } } return null } function generateRandomBox(id: number, existingBoxes: FloatingBox[]): FloatingBox | null { const position = generateNonOverlappingPosition(existingBoxes) if (!position) { return null } const movementType = Math.random() let driftX = 0 let driftY = 0 if (movementType > 0.4) { const driftAmount = (Math.random() > 0.5 ? 1 : -1) * 24 if (movementType > 0.7) { driftX = driftAmount } else { driftY = driftAmount } } return { id, type: Math.random() > 0.5 ? 'dots' : 'stripes', top: position.top, left: position.left, size: `${Math.random() * 60 + 40}px`, drift: { x: driftX, y: driftY }, } } function FloatingBoxes() { const [floatingBoxes, setFloatingBoxes] = useState([]) const boxIdRef = useRef(0) const lifetimeTimersRef = useRef>(new Set()) const containerRef = useRef(null) const isInView = useInView(containerRef, { once: false, amount: 0.3 }) useEffect(() => { if (!isInView) { // Clear all boxes when section goes out of view setFloatingBoxes([]) lifetimeTimersRef.current.forEach(clearTimeout) lifetimeTimersRef.current.clear() return } const lifetimeTimers = lifetimeTimersRef.current const spawnBox = () => { setFloatingBoxes((prev) => { const id = boxIdRef.current++ const newBox = generateRandomBox(id, prev) if (!newBox) { return prev } const lifetime = Math.random() * 2000 + 2000 const timerId = setTimeout(() => { setFloatingBoxes((current) => current.filter((box) => box.id !== id)) lifetimeTimers.delete(timerId) }, lifetime) lifetimeTimers.add(timerId) return [...prev, newBox] }) } const initialDelays = [0, 100, 200, 350, 500, 650, 800, 1000] const initialTimers = initialDelays.map((delay) => setTimeout(spawnBox, delay)) let intervalId: NodeJS.Timeout const scheduleNextSpawn = () => { intervalId = setTimeout( () => { spawnBox() scheduleNextSpawn() }, 300 + Math.random() * 300 ) } scheduleNextSpawn() return () => { initialTimers.forEach(clearTimeout) clearTimeout(intervalId) lifetimeTimers.forEach(clearTimeout) lifetimeTimers.clear() } }, [isInView]) return (
{floatingBoxes.map((box) => ( {box.type === 'dots' ? : } ))}
) } export const Intro = () => { // Calculate the scroll percentage ranges for each title const segmentSize = 100 / titles.length return (
{/* Floating boxes */} {/* Content - each title fades in/out based on scroll position */} {titles.map((title, index) => { const start = index * segmentSize const fadeInEnd = start + segmentSize * 0.2 const fadeOutStart = start + segmentSize * 0.8 const end = (index + 1) * segmentSize return ( 1 { start, end: fadeInEnd, to: { opacity: 1, filter: 'blur(0px)' }, }, // Fade out (except for last item) ...(index < titles.length - 1 ? [ { start: fadeOutStart, end, to: { opacity: 0, filter: 'blur(4px)' }, }, ] : []), ]} >

{title}

) })}
) }