{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "trusted-clients-showcase", "type": "registry:ui", "title": "Trusted Clients Showcase", "description": "Spotlight logo grid with staggered blur-up entrance and collective dim hover.", "dependencies": [], "files": [ { "path": "registry/ruixenui/trusted-clients-showcase.tsx", "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\n/* ═══════════════════════════════════════════════════════════\n Trusted Clients Showcase — Spotlight logo grid.\n\n Logos enter with a staggered blur-up reveal triggered by\n IntersectionObserver. At rest, all logos sit at 40% opacity\n — quiet, confident. Hover any logo and it lifts to full\n opacity while the rest dim to 20%, creating a theatrical\n spotlight effect. No framer-motion, no next/image — pure\n CSS transitions with React state for collective dim.\n\n Entrance: blur(4px) + translateY(8px) + opacity(0) → clear,\n staggered 60ms per item, cubic-bezier(0.16, 1, 0.3, 1).\n After entrance completes, transitions shorten to 250ms\n for snappy hover feedback.\n ═══════════════════════════════════════════════════════════ */\n\nexport interface LogoItem {\n name: string;\n logo?: React.ReactNode;\n href?: string;\n}\n\nexport interface TrustedClientsShowcaseProps {\n title?: string;\n clients?: LogoItem[];\n className?: string;\n}\n\nconst defaultClients: LogoItem[] = [\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 TrustedClientsShowcase({\n title = \"Trusted by industry leaders\",\n clients = defaultClients,\n className,\n}: TrustedClientsShowcaseProps) {\n const [hoveredIndex, setHoveredIndex] = React.useState(null);\n const [inView, setInView] = React.useState(false);\n const [entranceDone, setEntranceDone] = React.useState(false);\n const sectionRef = React.useRef(null);\n\n // Scroll-triggered entrance via IntersectionObserver\n React.useEffect(() => {\n const el = sectionRef.current;\n if (!el || typeof IntersectionObserver === \"undefined\") {\n setInView(true);\n return;\n }\n const observer = new IntersectionObserver(\n ([entry]) => {\n if (entry.isIntersecting) {\n setInView(true);\n observer.disconnect();\n }\n },\n { threshold: 0.1 },\n );\n observer.observe(el);\n return () => observer.disconnect();\n }, []);\n\n // Switch to snappy hover transitions after entrance stagger completes\n React.useEffect(() => {\n if (inView && !entranceDone) {\n const timer = setTimeout(\n () => setEntranceDone(true),\n clients.length * 60 + 600,\n );\n return () => clearTimeout(timer);\n }\n }, [inView, entranceDone, clients.length]);\n\n const anyHovered = hoveredIndex !== null;\n\n return (\n
\n
\n {title && (\n

\n {title}\n

\n )}\n setHoveredIndex(null)}\n >\n {clients.map((client, i) => {\n const isHovered = hoveredIndex === i;\n\n return (\n setHoveredIndex(i)}\n style={{\n opacity: inView\n ? anyHovered\n ? isHovered\n ? 1\n : 0.2\n : 0.4\n : 0,\n transform: inView\n ? isHovered\n ? \"translateY(-2px)\"\n : \"translateY(0)\"\n : \"translateY(8px)\",\n filter: inView ? \"blur(0px)\" : \"blur(4px)\",\n transition: entranceDone\n ? \"opacity 0.25s 0ms ease, transform 0.25s 0ms cubic-bezier(0.16, 1, 0.3, 1)\"\n : `opacity 0.5s ${i * 60}ms cubic-bezier(0.16, 1, 0.3, 1), transform 0.4s ${i * 60}ms cubic-bezier(0.16, 1, 0.3, 1), filter 0.5s ${i * 60}ms cubic-bezier(0.16, 1, 0.3, 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", "type": "registry:ui", "target": "components/ruixen/trusted-clients-showcase.tsx" } ] }