{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "client-carousel-showcase", "type": "registry:ui", "title": "Client Carousel Showcase", "description": "Infinite drift marquee with gradient edge masks — logos scroll continuously, pause on hover, and lift with exponential deceleration when focused.", "dependencies": [], "files": [ { "path": "registry/ruixenui/client-carousel-showcase.tsx", "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\n/* ═══════════════════════════════════════════════════════════\n Client Carousel Showcase — Proximity Ribbon.\n\n A continuous horizontal drift with a cursor-proximity field.\n Unlike binary hover (hovered vs not), the cursor creates a\n gaussian field of influence — logos NEAR your cursor brighten\n and lift proportionally, creating a smooth wave that follows\n your movement along the track.\n\n Fundamentally different from trusted-clients-showcase:\n • trusted-clients: STATIC grid, entrance stagger, binary\n collective dim (one bright, all others dark)\n • client-carousel: CONTINUOUS motion, proximity gradient\n (smooth distance-based falloff, no binary state)\n\n Proximity math:\n distance = |cursorX - logoCenterX|\n t = clamp(1 - distance/200, 0, 1)\n eased = t² × (3 - 2t) ← smoothstep\n scale = 1 + eased × 0.12 ← up to 1.12\n opacity = 0.35 + eased × 0.65 ← 0.35 → 1.0\n translateY = -eased × 4px ← lift toward cursor\n\n The smoothstep function has zero derivative at both ends —\n the glow fades in AND out smoothly, no hard cutoff.\n\n Track: CSS @keyframes translateX(0 → -33.333%) on a tripled set.\n Three copies ensure the track always exceeds viewport + one\n full copy, eliminating gaps on wide screens.\n Pause: animation-play-state toggled via mouseover/leave.\n Masks: linear-gradient edge fade, 8%–92% visibility window.\n Curve: cubic-bezier(0.22, 1, 0.36, 1) on individual items\n for smooth settling when the proximity field moves away.\n\n Zero dependencies. No embla, no framer-motion, no next/image.\n ═══════════════════════════════════════════════════════════ */\n\nexport interface ClientCarouselItem {\n name: string;\n logo?: React.ReactNode;\n href?: string;\n}\n\nexport interface ClientCarouselShowcaseProps {\n title?: string;\n clients?: ClientCarouselItem[];\n /** Seconds for one full scroll cycle. Lower = faster. */\n speed?: number;\n /** Reverse scroll direction. */\n reverse?: boolean;\n className?: string;\n}\n\nconst defaultClients: ClientCarouselItem[] = [\n { name: \"Vercel\" },\n { name: \"Linear\" },\n { name: \"Stripe\" },\n { name: \"Notion\" },\n { name: \"Figma\" },\n { name: \"Raycast\" },\n { name: \"Loom\" },\n { name: \"Pitch\" },\n];\n\nexport function ClientCarouselShowcase({\n title = \"Trusted by leading teams\",\n clients = defaultClients,\n speed = 30,\n reverse = false,\n className,\n}: ClientCarouselShowcaseProps) {\n // Triple for viewport coverage — at any scroll offset,\n // at least 2 copies fill the visible area.\n const tripled = React.useMemo(\n () => [...clients, ...clients, ...clients],\n [clients],\n );\n\n const containerRef = React.useRef(null);\n const itemRefs = React.useRef<(HTMLDivElement | null)[]>([]);\n const [paused, setPaused] = React.useState(false);\n\n // Proximity field: cursor position drives a smooth gaussian\n // falloff on each logo's scale, opacity, and Y-offset.\n // Direct DOM manipulation for 60fps — no React re-renders.\n const handleMouseMove = React.useCallback(\n (e: React.MouseEvent) => {\n const container = containerRef.current;\n if (!container) return;\n const containerRect = container.getBoundingClientRect();\n const mouseX = e.clientX - containerRect.left;\n\n for (let i = 0; i < itemRefs.current.length; i++) {\n const el = itemRefs.current[i];\n if (!el) continue;\n const rect = el.getBoundingClientRect();\n const centerX = rect.left + rect.width / 2 - containerRect.left;\n const distance = Math.abs(mouseX - centerX);\n const maxInfluence = 200;\n const t = Math.max(0, 1 - distance / maxInfluence);\n\n // Smoothstep: zero derivative at both ends for natural falloff\n const eased = t * t * (3 - 2 * t);\n\n const scale = 1 + eased * 0.12;\n const opacity = 0.35 + eased * 0.65;\n const y = -eased * 4;\n\n el.style.transform = `scale(${scale}) translateY(${y}px)`;\n el.style.opacity = String(opacity);\n }\n },\n [],\n );\n\n const resetItems = React.useCallback(() => {\n for (let i = 0; i < itemRefs.current.length; i++) {\n const el = itemRefs.current[i];\n if (!el) continue;\n el.style.transform = \"scale(1) translateY(0)\";\n el.style.opacity = \"0.35\";\n }\n }, []);\n\n const handleMouseEnter = React.useCallback(() => {\n setPaused(true);\n }, []);\n\n const handleMouseLeave = React.useCallback(() => {\n setPaused(false);\n resetItems();\n }, [resetItems]);\n\n return (\n
\n
\n {title && (\n

\n {title}\n

\n )}\n\n {/* Marquee container — gradient edge masks + vertical breathing room */}\n \n {/* Scrolling track */}\n \n {tripled.map((client, i) => (\n {\n itemRefs.current[i] = el;\n }}\n className=\"flex shrink-0 items-center justify-center pl-14 sm:pl-16\"\n style={{\n opacity: 0.35,\n transition:\n \"opacity 0.35s cubic-bezier(0.22, 1, 0.36, 1), transform 0.35s cubic-bezier(0.22, 1, 0.36, 1)\",\n }}\n >\n {client.href ? (\n \n {client.logo || (\n \n {client.name}\n \n )}\n \n ) : (\n \n {client.logo || (\n \n {client.name}\n \n )}\n \n )}\n
\n ))}\n \n \n \n\n \n
\n );\n}\n", "type": "registry:ui", "target": "components/ruixen/client-carousel-showcase.tsx" } ] }