"use client"; import { PauseIcon, PlayIcon, type LucideIcon } from "lucide-react"; import { motion, useAnimationFrame, useMotionValue, useReducedMotion } from "motion/react"; import { type PointerEvent, useEffect, useRef, useState } from "react"; import { cn } from "@/lib/utils"; export type WordmarkStyle = "sans" | "serif" | "mono" | "caps" | "heavy" | "light"; export interface TickerLogo { name: string; icon: LucideIcon; style?: WordmarkStyle; } export interface WordmarkTickerProps { logos: TickerLogo[]; title?: string; rows?: number; speed?: number; className?: string; } const wordmarkStyles: Record = { sans: "font-semibold text-lg tracking-tight", serif: "font-serif text-xl italic", mono: "font-mono font-medium text-base lowercase", caps: "font-bold text-sm uppercase tracking-[0.22em]", heavy: "font-black text-xl tracking-tighter", light: "font-light text-xl tracking-wide", }; function Wordmark({ logo }: { logo: TickerLogo }) { const Icon = logo.icon; return ( {logo.name} ); } function TickerRow({ logos, speed, reverse, paused, }: { logos: TickerLogo[]; speed: number; reverse: boolean; paused: boolean; }) { const reduce = useReducedMotion(); const containerRef = useRef(null); const copyRef = useRef(null); const [copyWidth, setCopyWidth] = useState(0); const [copies, setCopies] = useState(2); const x = useMotionValue(0); const velocity = useRef(0); const hovered = useRef(false); const drag = useRef<{ active: boolean; lastX: number; lastT: number; v: number } | null>(null); useEffect(() => { const container = containerRef.current; const copy = copyRef.current; if (!container || !copy) return; const measure = () => { const w = copy.offsetWidth; setCopyWidth(w); if (w > 0) setCopies(Math.max(2, Math.ceil(container.offsetWidth / w) + 1)); }; measure(); const observer = new ResizeObserver(measure); observer.observe(container); observer.observe(copy); return () => observer.disconnect(); }, []); const wrap = (value: number) => (copyWidth ? (((value % copyWidth) - copyWidth) % copyWidth) : value); useAnimationFrame((_, delta) => { if (!copyWidth || drag.current?.active) return; const dt = Math.min(delta, 50) / 1000; const cruise = reverse ? speed : -speed; const target = hovered.current || paused || reduce ? 0 : cruise; const gap = Math.abs(velocity.current - target); const rate = gap > speed * 3 ? 2.2 : hovered.current ? 5 : 1.6; velocity.current += (target - velocity.current) * (1 - Math.exp(-rate * dt)); if (target === 0 && Math.abs(velocity.current) < 0.5) { velocity.current = 0; return; } x.set(wrap(x.get() + velocity.current * dt)); }); const onPointerDown = (event: PointerEvent) => { if (event.button !== 0) return; event.currentTarget.setPointerCapture(event.pointerId); drag.current = { active: true, lastX: event.clientX, lastT: event.timeStamp, v: 0 }; velocity.current = 0; }; const onPointerMove = (event: PointerEvent) => { const d = drag.current; if (!d?.active) return; const dx = event.clientX - d.lastX; const dt = Math.max(1, event.timeStamp - d.lastT); d.v = d.v * 0.5 + (dx / dt) * 1000 * 0.5; d.lastX = event.clientX; d.lastT = event.timeStamp; x.set(wrap(x.get() + dx)); }; const onPointerUp = (event: PointerEvent) => { const d = drag.current; if (!d?.active) return; const idle = event.timeStamp - d.lastT > 90; velocity.current = idle ? 0 : Math.max(-4000, Math.min(4000, d.v)); drag.current = null; }; return (
{ if (e.pointerType === "mouse") hovered.current = true; }} onPointerLeave={() => { hovered.current = false; }} onPointerMove={onPointerMove} onPointerUp={onPointerUp} ref={containerRef} > {Array.from({ length: copies }, (_, copy) => { const items = logos.map((logo) => (
  • )); return copy === 0 ? (
      {items}
    ) : (
      {items}
    ); })}
    ); } export function WordmarkTicker({ logos, title = "Trusted by teams at", rows = 1, speed = 40, className }: WordmarkTickerProps) { const [paused, setPaused] = useState(false); const rowCount = Math.max(1, Math.min(rows, logos.length)); const split = Array.from({ length: rowCount }, (_, r) => logos.filter((_, i) => i % rowCount === r)); return (

    {title}

    {split.map((rowLogos, r) => ( l.name).join("|")} logos={rowLogos} paused={paused} reverse={r % 2 === 1} speed={speed * (r % 2 === 1 ? 0.8 : 1)} /> ))}
    ); }