'use client' import { useId, type CSSProperties } from 'react' import { cn } from '../../lib/cn' import { VisuallyHidden } from '../VisuallyHidden' export interface GooeyLoaderProps { /** What is loading. Announced; the blobs are decorative. */ label: string /** Overall size in pixels. */ size?: number /** How many blobs orbit. Three or four reads best. */ count?: number /** Any CSS colour for the blobs. */ color?: string /** Seconds for one orbit. */ duration?: number /** How much the blobs merge. Higher fuses them into one mass. */ goo?: number /** Merged last, so it wins. */ className?: string } /** * Blobs that orbit, merge into one mass, and separate again. * * The merging is the metaball trick: blur the whole group heavily, then push * the alpha channel through a steep contrast curve. Blurred edges that overlap * sum to a higher alpha than either alone, so the contrast step snaps them into * a single connected shape — and where they do not overlap, the same step * sharpens each blob's own edge back to a clean circle. * * That is why the filter has to wrap the *group*. Applied per blob it does * nothing at all, which is the mistake that makes people conclude the effect * does not work. * * The `feColorMatrix` row is `0 0 0 18 -7`: multiply alpha by 18, subtract 7. * Steeper fuses earlier and blows out the edges; shallower never quite joins. */ export function GooeyLoader({ label, size = 96, count = 4, color = 'var(--color-accent-strong)', duration = 2.6, goo = 18, className, }: GooeyLoaderProps) { const id = `goo-${useId().replace(/:/g, '')}` const blob = size * 0.3 const orbit = size * 0.26 return (
{/* The filter has to wrap the group. Per blob it does nothing. */}
{Array.from({ length: count }, (_, index) => { const angle = (Math.PI * 2 * index) / count return ( ) })}
{label}
) }