Fazekit

Code

"use client";
import * as React from "react";
import { motion, useReducedMotion } from "motion/react";
import { cn } from "@/lib/utils";

/* ---------- Invented wordmarks (drawn in SVG, colour = currentColor) ---------- */

type MarkProps = { className?: string };

function Wordmark({
  name,
  mark,
  width,
  weight = 600,
  spacing = -0.4,
  italic,
  upper,
  className,
}: {
  name: string;
  mark: React.ReactNode;
  width: number;
  weight?: number;
  spacing?: number;
  italic?: boolean;
  upper?: boolean;
  className?: string;
}) {
  return (
    <svg viewBox={`0 0 ${width} 32`} height="32" width={width} className={cn("h-7 w-auto overflow-visible sm:h-8", className)} role="img" aria-label={name}>
      <g fill="currentColor">{mark}</g>
      <text
        x="34"
        y="22.5"
        fill="currentColor"
        fontSize="19"
        fontWeight={weight}
        letterSpacing={spacing}
        fontStyle={italic ? "italic" : undefined}
        style={{ fontFamily: "inherit" }}
      >
        {upper ? name.toUpperCase() : name}
      </text>
    </svg>
  );
}

export const LOGOS: Record<string, (p: MarkProps) => React.ReactElement> = {
  Northwind: (p) => <Wordmark {...p} name="Northwind" width={140} mark={<path d="M13 3 22 28l-9-5-9 5z" />} />,
  Acme: (p) => (
    <Wordmark
      {...p}
      name="acme"
      width={96}
      weight={800}
      spacing={-1}
      mark={
        <>
          <circle cx="10" cy="16" r="8" />
          <circle cx="19" cy="16" r="8" fillOpacity="0.45" />
        </>
      }
    />
  ),
  Lumen: (p) => (
    <Wordmark
      {...p}
      name="Lumen"
      width={110}
      weight={500}
      spacing={1.5}
      upper
      mark={
        <>
          <circle cx="14" cy="16" r="5" />
          <path d="M14 3v4M14 25v4M1 16h4M23 16h4M5 7l3 3M20 22l3 3M5 25l3-3M20 10l3-3" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" />
        </>
      }
    />
  ),
  Orbit: (p) => (
    <Wordmark
      {...p}
      name="orbit"
      width={96}
      weight={700}
      mark={
        <>
          <circle cx="14" cy="16" r="5" />
          <ellipse cx="14" cy="16" rx="13" ry="6" fill="none" stroke="currentColor" strokeWidth="2" transform="rotate(-25 14 16)" />
        </>
      }
    />
  ),
  Vertex: (p) => (
    <Wordmark
      {...p}
      name="Vertex"
      width={112}
      weight={650}
      mark={
        <>
          <path d="M14 4 26 11 14 18 2 11z" />
          <path d="M2 17l12 7 12-7" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinejoin="round" />
        </>
      }
    />
  ),
  Quanta: (p) => (
    <Wordmark
      {...p}
      name="Quanta"
      width={118}
      weight={600}
      italic
      mark={
        <>
          <rect x="3" y="5" width="22" height="22" rx="6" />
          <rect x="9" y="11" width="10" height="10" rx="2" transform="rotate(45 14 16)" fill="var(--background)" />
        </>
      }
    />
  ),
  Helix: (p) => (
    <Wordmark
      {...p}
      name="helix"
      width={90}
      weight={700}
      spacing={-0.8}
      mark={
        <path
          d="M3 9c5 0 5 14 11 14s6-14 11-14M3 23c5 0 5-14 11-14s6 14 11 14"
          fill="none"
          stroke="currentColor"
          strokeWidth="2.6"
          strokeLinecap="round"
        />
      }
    />
  ),
  Nimbus: (p) => (
    <Wordmark
      {...p}
      name="Nimbus"
      width={118}
      weight={550}
      mark={<path d="M8 24a6 6 0 0 1-.6-12A8 8 0 0 1 22.5 10 6 6 0 0 1 21 24z" />}
    />
  ),
  Polar: (p) => (
    <Wordmark
      {...p}
      name="POLAR"
      width={108}
      weight={800}
      spacing={2}
      mark={<path d="M14 3l3.2 9.6H27l-8 5.8 3 9.6-8-5.9-8 5.9 3-9.6-8-5.8h9.8z" />}
    />
  ),
  Kestrel: (p) => (
    <Wordmark
      {...p}
      name="Kestrel"
      width={120}
      weight={600}
      mark={<path d="M2 10c7 0 11 3 12 9 2-8 7-13 13-14-3 4-4 9-5 13-2 6-7 8-11 7l3-4c-4 0-9-4-12-11z" />}
    />
  ),
};

const DEFAULT_ROW_A = ["Northwind", "Acme", "Lumen", "Orbit", "Vertex", "Quanta", "Helix", "Nimbus", "Polar", "Kestrel"];
const DEFAULT_ROW_B = ["Helix", "Polar", "Acme", "Kestrel", "Lumen", "Northwind", "Nimbus", "Vertex", "Orbit", "Quanta"];

/* ---------- Marquee primitive ---------- */

export interface MarqueeProps {
  children: React.ReactNode;
  /** Seconds for one full loop — lower is faster. */
  speed?: number;
  direction?: "left" | "right";
  pauseOnHover?: boolean;
  /** Gap between items in px. */
  gap?: number;
  /** Repeat the children inside each track so short lists still fill very wide screens. */
  repeat?: number;
  className?: string;
}

export function Marquee({ children, speed = 40, direction = "left", pauseOnHover = true, gap = 56, repeat = 1, className }: MarqueeProps) {
  const reduce = useReducedMotion();
  const base = React.Children.toArray(children);
  const items = Array.from({ length: Math.max(1, repeat) }, () => base).flat();
  const track = (hidden: boolean) => (
    <ul
      aria-hidden={hidden || undefined}
      className="flex shrink-0 items-center"
      style={{ gap, paddingRight: gap }}
    >
      {items.map((c, i) => (
        <li key={i} className="shrink-0">
          {c}
        </li>
      ))}
    </ul>
  );
  if (reduce) {
    return (
      <div className={cn("flex flex-wrap items-center justify-center", className)} style={{ gap }}>
        {base}
      </div>
    );
  }
  return (
    <div
      className={cn(
        "group flex overflow-hidden [mask-image:linear-gradient(to_right,transparent,black_12%,black_88%,transparent)]",
        className,
      )}
    >
      <div
        className={cn("flex w-max animate-logos-marquee", pauseOnHover && "group-hover:[animation-play-state:paused]")}
        style={
          {
            "--logos-marquee-duration": `${speed}s`,
            animationDirection: direction === "right" ? "reverse" : "normal",
          } as React.CSSProperties
        }
      >
        {track(false)}
        {track(true)}
      </div>
    </div>
  );
}

/* ---------- Block ---------- */

export interface LogosMarqueeProps {
  eyebrow?: string;
  title?: React.ReactNode;
  description?: string;
  /** Small facts shown under the marquee. Pass [] to hide. */
  facts?: string[];
  /** Logos for each row. Pass any React nodes (SVGs, images) or names from the built-in LOGOS set. */
  rows?: (React.ReactNode | string)[][];
  /** Seconds per loop (one pass through the row) for the first row; subsequent rows run slightly slower. */
  speed?: number;
  /** Direction of the first row; rows alternate. */
  direction?: "left" | "right";
  pauseOnHover?: boolean;
  className?: string;
}

export function LogosMarquee({
  eyebrow = "Customers",
  title = (
    <>
      Trusted by 4,000+ teams, <span className="text-muted-foreground">from seed to IPO.</span>
    </>
  ),
  description = "Product, design and engineering teams ship faster with a workspace they actually enjoy.",
  facts = ["4.9 average rating", "SOC 2 Type II", "99.99% uptime"],
  rows = [DEFAULT_ROW_A, DEFAULT_ROW_B],
  speed = 36,
  direction = "left",
  pauseOnHover = true,
  className,
}: LogosMarqueeProps) {
  const reduce = useReducedMotion();
  const render = (l: React.ReactNode | string) => {
    if (typeof l !== "string") return l;
    const Logo = LOGOS[l];
    return Logo ? <Logo /> : <span className="text-lg font-semibold">{l}</span>;
  };
  return (
    <section className={cn("relative w-full overflow-hidden bg-background py-16 sm:py-20", className)}>
      <motion.div
        initial={{ opacity: 0, y: reduce ? 0 : 10 }}
        whileInView={{ opacity: 1, y: 0 }}
        viewport={{ once: true }}
        transition={{ duration: 0.6 }}
        className="mx-auto max-w-3xl px-4 text-center sm:px-6"
      >
        <p className="text-sm font-medium text-primary">{eyebrow}</p>
        <h2 className="mt-2 text-balance text-2xl font-semibold tracking-tight text-foreground sm:text-3xl">{title}</h2>
        {description && <p className="mx-auto mt-3 max-w-xl text-pretty text-muted-foreground">{description}</p>}
      </motion.div>
      <div className="mt-12 space-y-8 sm:space-y-10">
        {rows.map((row, i) => {
          const dir = i % 2 === 0 ? direction : direction === "left" ? "right" : "left";
          return (
            <Marquee key={i} speed={speed + i * 6} direction={dir} pauseOnHover={pauseOnHover} repeat={2}>
              {row.map((l, j) => (
                <span
                  key={j}
                  className="block text-foreground/55 transition-colors duration-300 hover:text-foreground"
                >
                  {render(l)}
                </span>
              ))}
            </Marquee>
          );
        })}
      </div>
      {facts.length > 0 && (
        <ul className="mx-auto mt-12 flex max-w-3xl flex-wrap items-center justify-center gap-x-6 gap-y-2 px-4 text-sm text-muted-foreground">
          {facts.map((f, i) => (
            <li key={f} className="flex items-center gap-6">
              {i > 0 && <span aria-hidden className="hidden size-1 rounded-full bg-muted-foreground/40 sm:block" />}
              {f}
            </li>
          ))}
        </ul>
      )}
    </section>
  );
}

More in Logo Clouds

View all →