Logo grid with auto-cycle pulse focus — active mark breathes, neighbors dim on hover.
@nusaiba
registry/blocks/logo-cloud/3/logo-cloud-section.tsx
"use client";
import { useCallback, useEffect, useState } from "react";
import { motion, useInView, useReducedMotion } from "motion/react";
import { BrandFonts, BrandWordmark } from "@/lib/brand-typography";
import { defaultSpring, motionStagger, motionViewport } from "@/lib/motion-tokens";
import { cn } from "@/lib/utils";
import { SectionShell, ContentRail } from "@/components/layout-contract";
const CYCLE_MS = 3200;
const logoIconClass =
"size-6 shrink-0 object-contain outline outline-1 -outline-offset-1 outline-black/10 dark:outline-white/10 md:size-[1.625rem]";
const logoWordmarkClass = "text-[0.8125rem] leading-none sm:text-[0.9375rem] md:text-base";
function getLogoDevUrl(domain: string) {
const token = process.env.NEXT_PUBLIC_LOGO_DEV_PUBLISHABLE_KEY;
if (!token) return null;
const params = new URLSearchParams({
token,
size: "96",
format: "png",
retina: "true",
fallback: "monogram",
});
return `https://img.logo.dev/${domain}?${params}`;
}
const logos = [
{ name: "Stripe", domain: "stripe.com" },
{ name: "Notion", domain: "notion.so" },
{ name: "Vercel", domain: "vercel.com" },
{ name: "Linear", domain: "linear.app" },
{ name: "Figma", domain: "figma.com" },
{ name: "Slack", domain: "slack.com" },
{ name: "GitHub", domain: "github.com" },
{ name: "Shopify", domain: "shopify.com" },
];
function PulseCell({
brand,
isActive,
hasFocus,
onActivate,
onDeactivate,
reduceMotion,
cellVariants,
}: {
brand: (typeof logos)[number];
isActive: boolean;
hasFocus: boolean;
onActivate: () => void;
onDeactivate: () => void;
reduceMotion: boolean;
cellVariants: {
hidden: { opacity: number; scale?: number; filter?: string };
visible: { opacity: number; scale?: number; filter?: string };
};
}) {
const src = getLogoDevUrl(brand.domain);
const dimmed = hasFocus ? !isActive : false;
const pulsing = isActive && !hasFocus && !reduceMotion;
return (
<motion.button
aria-label={brand.name}
aria-pressed={isActive}
className={cn(
"group relative flex min-h-[5.25rem] w-full items-center justify-center rounded-[var(--radius-md)] border border-border bg-background transition-[background-color,border-color,transform] active:scale-[0.96] sm:min-h-[5.75rem]",
"after:absolute after:top-1/2 after:left-1/2 after:size-11 after:-translate-1/2",
isActive && "border-foreground/10 bg-muted/15",
)}
onBlur={onDeactivate}
onFocus={onActivate}
onMouseEnter={onActivate}
onMouseLeave={onDeactivate}
type="button"
variants={cellVariants}
>
<motion.div
animate={
reduceMotion
? undefined
: dimmed
? { opacity: 0.42, scale: 0.97, filter: "blur(1px)" }
: pulsing
? { opacity: 1, scale: [1, 1.03, 1], filter: "blur(0px)" }
: { opacity: isActive ? 1 : 0.55, scale: 1, filter: "blur(0px)" }
}
className="flex h-7 items-center gap-2 px-2 sm:gap-2.5 md:h-8"
transition={
pulsing
? { duration: 2.4, repeat: Number.POSITIVE_INFINITY, ease: "easeInOut" }
: { type: "spring", duration: 0.32, bounce: 0 }
}
>
{src ? (
<img
alt=""
aria-hidden
className={logoIconClass}
height={26}
loading="lazy"
referrerPolicy="origin"
src={src}
/>
) : (
<span className="font-medium text-foreground/45 text-xs">{brand.name}</span>
)}
<BrandWordmark
className={cn(
logoWordmarkClass,
"hidden text-foreground/80 sm:block",
isActive && "text-foreground",
)}
domain={brand.domain}
name={brand.name}
/>
</motion.div>
</motion.button>
);
}
export function LogoCloudPulseSection() {
const reduceMotion = useReducedMotion();
const [rootNode, setRootNode] = useState<HTMLElement | null>(null);
const isInView = useInView(
{ current: rootNode },
{ ...motionViewport, amount: 0.35 },
);
const [active, setActive] = useState(0);
const [cycleActive, setCycleActive] = useState(false);
const [hoverFocus, setHoverFocus] = useState<string | null>(null);
useEffect(() => {
if (!isInView || reduceMotion) return;
setCycleActive(true);
}, [isInView, reduceMotion]);
useEffect(() => {
if (!cycleActive || reduceMotion || hoverFocus) return;
const timer = setInterval(() => {
setActive((current) => (current + 1) % logos.length);
}, CYCLE_MS);
return () => clearInterval(timer);
}, [cycleActive, hoverFocus, reduceMotion]);
const selectBrand = useCallback((domain: string, index: number) => {
setHoverFocus(domain);
setActive(index);
setCycleActive(false);
}, []);
const clearFocus = useCallback((domain: string) => {
setHoverFocus((current) => (current === domain ? null : current));
setCycleActive(true);
}, []);
const cellVariants = reduceMotion
? { hidden: { opacity: 1 }, visible: { opacity: 1 } }
: {
hidden: { opacity: 0, scale: 0.97, filter: "blur(4px)" },
visible: { opacity: 1, scale: 1, filter: "blur(0px)" },
};
return (
<SectionShell spacingMode="compact" className="bg-background" ref={setRootNode}>
<BrandFonts id="logo-cloud-3-brand-fonts" />
<ContentRail maxWidth="max-w-4xl" className="space-y-8 md:space-y-9">
<div className="mx-auto max-w-sm space-y-2 text-center">
<motion.p
className="font-medium text-primary text-xs uppercase tracking-[0.22em]"
initial={reduceMotion ? false : { opacity: 0, y: 8, filter: "blur(4px)" }}
transition={defaultSpring}
viewport={motionViewport}
whileInView={reduceMotion ? undefined : { opacity: 1, y: 0, filter: "blur(0px)" }}
>
Pulse grid
</motion.p>
<motion.p
className="text-balance font-medium text-lg tracking-tight md:text-xl"
initial={reduceMotion ? false : { opacity: 0, y: 8, filter: "blur(4px)" }}
transition={{ ...defaultSpring, delay: motionStagger.item }}
viewport={motionViewport}
whileInView={reduceMotion ? undefined : { opacity: 1, y: 0, filter: "blur(0px)" }}
>
In steady rotation
</motion.p>
<motion.p
className="text-pretty text-muted-foreground text-sm leading-relaxed"
initial={reduceMotion ? false : { opacity: 0, y: 8, filter: "blur(4px)" }}
transition={{ ...defaultSpring, delay: motionStagger.item * 2 }}
viewport={motionViewport}
whileInView={reduceMotion ? undefined : { opacity: 1, y: 0, filter: "blur(0px)" }}
>
Hover a cell or wait for the cycle to advance.
</motion.p>
</div>
<motion.div
animate={isInView ? "visible" : "hidden"}
className="grid grid-cols-2 gap-3 sm:grid-cols-4 md:gap-4"
initial="hidden"
onMouseLeave={() => setHoverFocus(null)}
variants={{
hidden: {},
visible: {
transition: {
staggerChildren: motionStagger.item,
delayChildren: 0.04,
},
},
}}
>
{logos.map((brand, index) => (
<PulseCell
brand={brand}
cellVariants={cellVariants}
hasFocus={hoverFocus !== null}
isActive={active === index}
key={brand.domain}
onActivate={() => selectBrand(brand.domain, index)}
onDeactivate={() => clearFocus(brand.domain)}
reduceMotion={!!reduceMotion}
/>
))}
</motion.div>
</ContentRail>
</SectionShell>
);
}


































