{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "logo-cloud", "title": "Logo Cloud", "description": "An animated logo cloud that cycles through logo sets with staggered scale, opacity, and blur transitions.", "files": [ { "path": "registry/custom-ui/logo-cloud.tsx", "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport interface LogoCloudItem {\n name: string;\n src?: string;\n alt?: string;\n href?: string;\n logo?: React.ReactNode;\n className?: string;\n}\n\nexport interface LogoCloudProps\n extends Omit, \"children\"> {\n logos: LogoCloudItem[];\n setSize?: number;\n interval?: number;\n pauseOnHover?: boolean;\n animateOnView?: boolean;\n cellClassName?: string;\n logoClassName?: string;\n gridClassName?: string;\n}\n\nconst DEFAULT_SET_SIZE = 6;\nconst DEFAULT_INTERVAL = 4000;\nconst DURATION = 500;\nconst STAGGER = 60;\nconst EASE_SCALE_OUT = \"cubic-bezier(0.55, 0, 0.85, 0.15)\";\nconst EASE_SCALE_IN = \"cubic-bezier(0.15, 0.85, 0.35, 1)\";\n\nfunction chunkLogos(logos: LogoCloudItem[], size: number) {\n if (logos.length <= size) return [logos];\n\n return Array.from({ length: Math.ceil(logos.length / size) }, (_, index) => {\n const set = logos.slice(index * size, index * size + size);\n\n while (set.length < size) {\n set.push(logos[(index * size + set.length) % logos.length]);\n }\n\n return set;\n });\n}\n\nfunction usePrefersReducedMotion() {\n const [prefersReducedMotion, setPrefersReducedMotion] = React.useState(false);\n\n React.useEffect(() => {\n const query = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n setPrefersReducedMotion(query.matches);\n\n const updatePreference = () => setPrefersReducedMotion(query.matches);\n query.addEventListener(\"change\", updatePreference);\n\n return () => query.removeEventListener(\"change\", updatePreference);\n }, []);\n\n return prefersReducedMotion;\n}\n\nfunction useInView(\n ref: React.RefObject,\n enabled: boolean,\n) {\n const [isInView, setIsInView] = React.useState(!enabled);\n\n React.useEffect(() => {\n const element = ref.current;\n if (!enabled || !element) {\n setIsInView(true);\n return;\n }\n\n const observer = new IntersectionObserver(\n ([entry]) => setIsInView(Boolean(entry?.isIntersecting)),\n { threshold: 0.3 },\n );\n\n observer.observe(element);\n return () => observer.disconnect();\n }, [enabled, ref]);\n\n return isInView;\n}\n\nfunction LogoCloud({\n className,\n logos,\n setSize = DEFAULT_SET_SIZE,\n interval = DEFAULT_INTERVAL,\n pauseOnHover = true,\n animateOnView = true,\n cellClassName,\n logoClassName,\n gridClassName,\n ...props\n}: LogoCloudProps) {\n const rootRef = React.useRef(null);\n const pausedRef = React.useRef(false);\n const prefersReducedMotion = usePrefersReducedMotion();\n const isInView = useInView(rootRef, animateOnView);\n\n const normalizedSetSize = Math.min(Math.max(1, setSize), logos.length || 1);\n const sets = React.useMemo(\n () => chunkLogos(logos, normalizedSetSize),\n [logos, normalizedSetSize],\n );\n const [activeSet, setActiveSet] = React.useState(0);\n\n React.useEffect(() => {\n setActiveSet(0);\n }, [logos, normalizedSetSize]);\n\n React.useEffect(() => {\n if (sets.length <= 1 || prefersReducedMotion || !isInView) return;\n\n const timer = window.setInterval(() => {\n if (!pausedRef.current) {\n setActiveSet((currentSet) => (currentSet + 1) % sets.length);\n }\n }, interval);\n\n return () => window.clearInterval(timer);\n }, [interval, isInView, prefersReducedMotion, sets.length]);\n\n if (logos.length === 0) return null;\n\n return (\n {\n if (pauseOnHover) pausedRef.current = true;\n }}\n onMouseLeave={() => {\n if (pauseOnHover) pausedRef.current = false;\n }}\n {...props}\n >\n \n {Array.from({ length: normalizedSetSize }, (_, position) => (\n \n {sets.map((set, setIndex) => {\n const logo = set[position];\n if (!logo) return null;\n\n const isActive = setIndex === activeSet;\n\n return (\n \n );\n })}\n \n ))}\n \n \n );\n}\n\nfunction LogoCloudLogo({\n item,\n isActive,\n position,\n logoClassName,\n}: {\n item: LogoCloudItem;\n isActive: boolean;\n position: number;\n logoClassName?: string;\n}) {\n const delay = isActive ? position * STAGGER + DURATION : position * STAGGER;\n const logo = item.logo ?? (\n <>\n {item.src ? (\n \n ) : (\n \n {item.name}\n \n )}\n \n );\n\n const content = item.href ? (\n \n {logo}\n \n ) : (\n logo\n );\n\n return (\n \n {content}\n \n );\n}\n\nexport { LogoCloud };\n", "type": "registry:ui", "target": "components/custom-ui/logo-cloud.tsx" } ], "categories": [ "marketing", "logos" ], "type": "registry:ui" }