{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "auto-scrolling-client-carousel", "type": "registry:ui", "title": "Auto Scrolling Client Carousel", "description": "Dual ribbon with grayscale bloom — two rows scroll in opposite directions, monochrome at rest, color blooms on hover with spring-bounce lift.", "dependencies": [], "files": [ { "path": "registry/ruixenui/auto-scrolling-client-carousel.tsx", "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\n/* ═══════════════════════════════════════════════════════════\n Auto Scrolling Client Carousel — Dual Ribbon, Grayscale Bloom.\n\n Two rows of logos scrolling in opposite directions at\n slightly offset speeds. Unlike the other two client\n showcases, this one keeps FULL OPACITY at all times —\n logos are always clearly visible. The \"muted\" state is\n achieved through CSS grayscale, not opacity dimming:\n monochrome at rest, color blooms on hover.\n\n Three paradigms, zero overlap:\n • trusted-clients: STATIC grid, binary OPACITY dim\n • client-carousel: SINGLE drift, proximity OPACITY field\n • auto-scrolling: DUAL ribbon, GRAYSCALE bloom (no opacity)\n\n Rows scroll continuously — they never pause, never stop.\n The interaction is purely visual: grayscale(1) → grayscale(0)\n with a spring-bounce scale lift. The motion stays constant.\n\n Dual-ribbon rhythm:\n Row 1: clients in order, scrolling left, speed N\n Row 2: clients reversed, scrolling right, speed N × 1.15\n The slight speed offset creates a visual polyrhythm —\n logos phase in and out of vertical alignment.\n\n Scale lift: cubic-bezier(0.34, 1.56, 0.64, 1) — spring with\n slight overshoot. Different from trusted-clients' deceleration\n and client-carousel's exponential ease.\n\n Seamless loop: three copies of items (not two). With only 2×,\n the track can be shorter than the viewport at the loop point,\n creating a visible gap. With 3×, the track always exceeds\n viewport + one full copy. Per-item padding-left (not flex gap)\n ensures -33.333% lands exactly at one copy boundary.\n\n Zero dependencies. No embla, no framer-motion, no next/image.\n ═══════════════════════════════════════════════════════════ */\n\nexport interface AutoScrollingCarouselItem {\n name: string;\n logo?: React.ReactNode;\n href?: string;\n}\n\nexport interface AutoScrollingClientCarouselProps {\n title?: string;\n clients?: AutoScrollingCarouselItem[];\n /** Seconds for one full scroll cycle. Lower = faster. */\n speed?: number;\n className?: string;\n}\n\nconst defaultClients: AutoScrollingCarouselItem[] = [\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\n/* ── Single ribbon row ─────────────────────────────────── */\n\nfunction Ribbon({\n items,\n speed,\n reverse,\n}: {\n items: AutoScrollingCarouselItem[];\n speed: number;\n reverse: boolean;\n}) {\n // Triple for viewport coverage — at any scroll offset,\n // at least 2 copies fill the visible area.\n const tripled = [...items, ...items, ...items];\n\n return (\n
\n {title}\n
\n )}\n\n