Files
supabase/apps/www/components/Wrapped/AnimatedGridBackground.tsx
Ivan VasilovandJordi Enric 56de26fe22 chore: Migrate the monorepo to use Tailwind v4 (#45318)
This PR migrates the whole monorepo to use Tailwind v4:
- Removed `@tailwindcss/container-queries` plugin since it's included by
default in v4,
- Bump all instances of Tailwind to v4. Made minimal changes to the
shared config to remove non-supported features (`alpha` mentions),
- Migrate all apps to be compatible with v4 configs,
- Fix the `typography.css` import in 3 apps,
- Add missing rules which were included by default in v3,
- Run `pnpm dlx @tailwindcss/upgrade` on all apps, which renames a lot
of classes
- Rename all misnamed classes according to
https://tailwindcss.com/docs/upgrade-guide#renamed-utilities in all
apps.

---------

Co-authored-by: Jordi Enric <jordi.err@gmail.com>
2026-04-30 10:53:24 +00:00

233 lines
7.1 KiB
TypeScript

'use client'
import { motion, useInView } from 'framer-motion'
import { useState, useEffect, useCallback, useMemo, useRef } from 'react'
import { Dots, Stripes } from './Visuals'
const STAGGER_DELAY = 0.05
const MOVE_INTERVAL = 2000 // Time between moves in ms
type TileType = 'dots' | 'stripes'
type TileConfig = {
id: number
type: TileType
initialCell: number
}
type ResponsiveRows = {
mobile: number
desktop: number
}
type AnimatedGridBackgroundProps = {
cols: number
rows: number | ResponsiveRows
tiles: Array<{ cell: number; type: TileType }>
initialDelay?: number
}
export function AnimatedGridBackground({
cols,
rows,
tiles,
initialDelay = 0,
}: AnimatedGridBackgroundProps) {
// Detect if we're on mobile (client-side only)
const [isMobile, setIsMobile] = useState(false)
useEffect(() => {
const checkMobile = () => setIsMobile(window.innerWidth < 768)
checkMobile()
window.addEventListener('resize', checkMobile)
return () => window.removeEventListener('resize', checkMobile)
}, [])
// Get the actual row count based on responsive config
const actualRows = typeof rows === 'number' ? rows : isMobile ? rows.mobile : rows.desktop
const totalCells = cols * actualRows
// Convert tiles config to internal format with IDs, filtering out tiles outside bounds
const tileConfigs: TileConfig[] = useMemo(
() =>
tiles
.filter((tile) => tile.cell < totalCells)
.map((tile, index) => ({
id: index,
type: tile.type,
initialCell: tile.cell,
})),
[tiles, totalCells]
)
// Track current positions of all tiles
const [tilePositions, setTilePositions] = useState<Map<number, number>>(new Map())
// Reinitialize tile positions when tileConfigs changes (e.g., on responsive change)
useEffect(() => {
const initial = new Map<number, number>()
tileConfigs.forEach((tile) => {
initial.set(tile.id, tile.initialCell)
})
setTilePositions(initial)
}, [tileConfigs])
// Get adjacent cells (up, down, left, right) that are within bounds
const getAdjacentCells = useCallback(
(cell: number): number[] => {
const row = Math.floor(cell / cols)
const col = cell % cols
const adjacent: number[] = []
// Up
if (row > 0) adjacent.push(cell - cols)
// Down
if (row < actualRows - 1) adjacent.push(cell + cols)
// Left
if (col > 0) adjacent.push(cell - 1)
// Right
if (col < cols - 1) adjacent.push(cell + 1)
return adjacent
},
[cols, actualRows]
)
// Get occupied cells
const getOccupiedCells = useCallback((): Set<number> => {
return new Set(tilePositions.values())
}, [tilePositions])
// Move a random tile to an adjacent empty cell
const moveRandomTile = useCallback(() => {
const occupiedCells = getOccupiedCells()
const tileIds = Array.from(tilePositions.keys())
// Shuffle tile IDs to pick randomly
const shuffledIds = [...tileIds].sort(() => Math.random() - 0.5)
for (const tileId of shuffledIds) {
const currentCell = tilePositions.get(tileId)!
const adjacentCells = getAdjacentCells(currentCell)
// Find empty adjacent cells
const emptyAdjacent = adjacentCells.filter((cell) => !occupiedCells.has(cell))
if (emptyAdjacent.length > 0) {
// Pick a random empty adjacent cell
const targetCell = emptyAdjacent[Math.floor(Math.random() * emptyAdjacent.length)]
setTilePositions((prev) => {
const next = new Map(prev)
next.set(tileId, targetCell)
return next
})
return // Only move one tile per interval
}
}
}, [tilePositions, getAdjacentCells, getOccupiedCells])
// Ref for viewport detection
const containerRef = useRef<HTMLDivElement>(null)
const isInView = useInView(containerRef, { once: true, amount: 0.2 })
// Set up the movement interval (only when in view)
useEffect(() => {
if (!isInView) return
const interval = setInterval(moveRandomTile, MOVE_INTERVAL)
return () => clearInterval(interval)
}, [moveRandomTile, isInView])
// Calculate position offset for animation
const getCellPosition = (cell: number) => {
const row = Math.floor(cell / cols)
const col = cell % cols
return { row, col }
}
// Calculate the CSS transform for a tile based on its current cell vs initial cell
const getTileTransform = (tileId: number, initialCell: number) => {
const currentCell = tilePositions.get(tileId) ?? initialCell
const initial = getCellPosition(initialCell)
const current = getCellPosition(currentCell)
const colDiff = current.col - initial.col
const rowDiff = current.row - initial.row
// Account for 1px border between cells
const xOffset = colDiff * 100
const yOffset = rowDiff * 100
const xBorderOffset = colDiff
const yBorderOffset = rowDiff
return {
x: `calc(${xOffset}% + ${xBorderOffset}px)`,
y: `calc(${yOffset}% + ${yBorderOffset}px)`,
}
}
return (
<div
ref={containerRef}
className={`absolute inset-0 grid h-full *:border-muted *:border-r *:border-b overflow-hidden`}
style={{
gridTemplateColumns: `repeat(${cols}, 1fr)`,
gridTemplateRows: `repeat(${actualRows}, 1fr)`,
}}
>
{Array.from({ length: totalCells }).map((_, cellIndex) => {
const row = Math.floor(cellIndex / cols)
const col = cellIndex % cols
const diagonalIndex = row + col
// Check if this cell is the initial position of any tile
const tile = tileConfigs.find((t) => t.initialCell === cellIndex)
// Remove right border on last column
const isLastCol = col === cols - 1
// Remove bottom border on last row
const isLastRow = row === actualRows - 1
return (
<div
key={cellIndex}
className={`relative ${isLastCol ? 'border-r-0!' : ''} ${isLastRow ? 'border-b-0!' : ''}`}
>
{tile && (
<motion.div
className="absolute inset-0"
initial={{ opacity: 0, scale: 0.98 }}
animate={
isInView
? {
opacity: 1,
scale: 1,
x: getTileTransform(tile.id, tile.initialCell).x,
y: getTileTransform(tile.id, tile.initialCell).y,
}
: { opacity: 0, scale: 0.98 }
}
transition={{
opacity: {
delay: initialDelay + diagonalIndex * STAGGER_DELAY,
duration: 0.3,
},
scale: {
delay: initialDelay + diagonalIndex * STAGGER_DELAY,
duration: 0.3,
},
x: { type: 'spring', duration: 0.69, bounce: 0.12 },
y: { type: 'spring', duration: 0.69, bounce: 0.12 },
}}
>
{tile.type === 'dots' ? <Dots /> : <Stripes />}
</motion.div>
)}
</div>
)
})}
</div>
)
}