"use client"; import { motion, useReducedMotion } from "motion/react"; import type { ReactNode } from "react"; import { Marquee } from "@/components/motion/marquee"; import { EASE_OUT } from "@/lib/ease"; import { cn } from "@/lib/utils"; export interface LogoCloudItem { id: string; /** Accessible name of the company or product. */ name: string; /** SVG or any node. Use `currentColor` so it follows the muted/hover tint. */ logo: ReactNode; href?: string; } export interface LogoCloudProps { logos: LogoCloudItem[]; /** "marquee" scrolls one infinite row; "grid" lays logos out and fades them in. Default "marquee". */ variant?: "marquee" | "grid"; eyebrow?: ReactNode; heading?: ReactNode; description?: ReactNode; /** Seconds for one marquee loop. Default 30. */ speed?: number; /** Marquee direction. Default "left". */ direction?: "left" | "right"; /** Pause the marquee on hover or keyboard focus. Default true. */ pauseOnHover?: boolean; /** Logos start muted (grayscale, dimmed) and take their own colors on hover. Default true. */ colorize?: boolean; /** Grid columns at the widest container size. Default 4. */ columns?: 3 | 4 | 5 | 6; className?: string; /** Classes for each logo tile (sizing, padding). */ itemClassName?: string; } // Static class maps: Tailwind only ships classes it can see written out. const GRID_COLUMNS: Record<3 | 4 | 5 | 6, string> = { 3: "grid-cols-2 @md:grid-cols-3", 4: "grid-cols-2 @md:grid-cols-4", 5: "grid-cols-2 @md:grid-cols-3 @2xl:grid-cols-5", 6: "grid-cols-3 @md:grid-cols-4 @2xl:grid-cols-6", }; const STAGGER = 0.05; function LogoMark({ item, colorize, lift, className, }: { item: LogoCloudItem; colorize: boolean; lift: boolean; className?: string; }) { const classes = cn( "group/logo flex h-12 items-center justify-center px-3 text-muted-foreground outline-none transition-[color,filter,opacity,translate,border-color,box-shadow] duration-300 [&_svg]:h-7 [&_svg]:w-auto [&_svg]:max-w-full", colorize && "opacity-70 grayscale hover:text-foreground hover:opacity-100 hover:grayscale-0 focus-visible:text-foreground focus-visible:opacity-100 focus-visible:grayscale-0", // Tailwind's hover variant only applies on hover-capable pointers, so // touch devices never get a stuck lift. lift && "hover:-translate-y-1 motion-reduce:hover:translate-y-0", item.href && "rounded-xl focus-visible:ring-2 focus-visible:ring-ring/50", className, ); const content = ( <> {item.logo} {item.name} ); return item.href ? ( {content} ) : (
{content}
); } /** * "Trusted by" logo strip. The marquee variant reuses `Marquee` for one * infinite row behind edge fade masks, pausing on hover or focus. The grid * variant fades logos in with a stagger when it scrolls into view; each * logo lifts on hover. Logos start muted and take their own colors on hover. * Reduced motion shows the marquee's logos as a static wrapped row. */ export function LogoCloud({ logos, variant = "marquee", eyebrow, heading, description, speed = 30, direction = "left", pauseOnHover = true, colorize = true, columns = 4, className, itemClassName, }: LogoCloudProps) { const reduce = useReducedMotion(); const header = eyebrow || heading || description ? (
{eyebrow ? (

{eyebrow}

) : null} {heading ? (

{heading}

) : null} {description ? (

{description}

) : null}
) : null; let body: ReactNode; if (variant === "marquee" && !reduce) { body = ( div]:[animation-play-state:paused]", )} > {logos.map((item) => ( ))} ); } else if (variant === "marquee") { // Reduced motion: no endless movement, just a centered wrapped row. body = ( ); } else { body = ( {logos.map((item) => ( ))} ); } return (
{header} {body}
); }