- "content": "\"use client\";\n\nimport type { ComponentProps, ReactNode } from \"react\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\nconst DEFAULT_LOGOS = [\n \"Acme\",\n \"Globex\",\n \"Soylent\",\n \"Initech\",\n \"Hooli\",\n \"Umbrella\",\n \"Stark\",\n \"Wayne\",\n \"Cyberdyne\",\n \"Wonka\",\n \"Tyrell\",\n \"Aperture\",\n];\n\ntype LogoMarqueeRowProps = Omit<ComponentProps<\"div\">, \"children\"> & {\n /** Brand names rendered as the ticker items. Repeated to keep the loop seamless. */\n logos?: ReadonlyArray<string>;\n /** Loop duration in seconds. Lower = faster. Clamped to a positive number. */\n speed?: number;\n /** Scroll direction. */\n direction?: \"left\" | \"right\";\n /**\n * Accessible name for the marquee. Surfaces to screen readers as the\n * row's label (e.g. \"Customer logos\", \"Trusted by\"). Defaults to a\n * sensible generic so the row isn't anonymous in AT output.\n */\n label?: string;\n};\n\nexport function LogoMarqueeRow({\n logos = DEFAULT_LOGOS,\n speed = 32,\n direction = \"left\",\n label = \"Customer logos\",\n className,\n ...rest\n}: LogoMarqueeRowProps) {\n const reduced = useReducedMotion();\n const safeSpeed = Number.isFinite(speed) && speed > 0 ? speed : 32;\n\n // Two copies of the list keep the loop seamless: when the first copy has\n // moved by exactly its own width, the second copy is in the same spot.\n const items = [...logos, ...logos];\n const xRange = direction === \"left\" ? [\"0%\", \"-50%\"] : [\"-50%\", \"0%\"];\n\n return (\n <div\n className={cn(\n \"relative w-full overflow-hidden\",\n \"[mask-image:linear-gradient(to_right,transparent,black_12%,black_88%,transparent)]\",\n className,\n )}\n {...rest}\n >\n <motion.ul\n aria-label={label}\n className=\"flex w-max items-center gap-12 whitespace-nowrap text-base font-medium text-white/55\"\n initial={false}\n animate={reduced ? { x: \"0%\" } : { x: xRange }}\n transition={\n reduced\n ? undefined\n : { duration: safeSpeed, ease: \"linear\", repeat: Infinity }\n }\n >\n {items.map((logo, i) => (\n <li\n key={`${logo}-${i}`}\n className=\"flex shrink-0 items-center gap-3 text-xl tracking-tight\"\n aria-hidden={i >= logos.length || undefined}\n >\n <span className=\"inline-block h-2 w-2 rounded-full bg-white/30\" aria-hidden />\n {logo}\n </li>\n ))}\n </motion.ul>\n </div>\n );\n}\n\ntype LogoMarqueeHeroProps = Omit<ComponentProps<\"section\">, \"children\"> & {\n children?: ReactNode;\n /** Logos for the top row. */\n logos?: ReadonlyArray<string>;\n /** Optional second row. Defaults to a reversed copy of `logos`. Pass `null` to hide. */\n secondaryLogos?: ReadonlyArray<string> | null;\n /** Row scroll speed in seconds. */\n speed?: number;\n};\n\nexport function LogoMarqueeHero({\n children,\n logos = DEFAULT_LOGOS,\n secondaryLogos,\n speed = 32,\n className,\n ...rest\n}: LogoMarqueeHeroProps) {\n // `undefined` means \"use the reversed default\"; `null` means \"hide it\".\n const showSecondary = secondaryLogos !== null;\n const resolvedSecondary =\n secondaryLogos === undefined ? [...logos].reverse() : secondaryLogos;\n\n return (\n <section\n {...rest}\n className={cn(\n \"relative isolate flex min-h-[100svh] w-full flex-col items-center justify-center overflow-hidden bg-[#08080a] text-white\",\n className,\n )}\n >\n <div className=\"absolute inset-0 -z-10 bg-[radial-gradient(ellipse_at_top,_rgba(80,80,120,0.18)_0%,_transparent_60%)]\" />\n <div className=\"relative z-10 mx-auto flex w-full max-w-3xl flex-col items-center px-6 text-center\">\n {children ?? <DefaultMarqueeContent />}\n </div>\n <div\n data-slot=\"marquee\"\n className={cn(\n \"relative z-10 mt-12 flex w-full flex-col gap-6\",\n showSecondary ? \"sm:mt-16\" : \"sm:mt-20\",\n )}\n >\n <LogoMarqueeRow\n logos={logos}\n speed={speed}\n direction=\"left\"\n label=\"Customer logos\"\n />\n {showSecondary && resolvedSecondary && resolvedSecondary.length > 0 ? (\n <LogoMarqueeRow\n logos={resolvedSecondary}\n speed={speed * 1.15}\n direction=\"right\"\n label=\"More customer logos\"\n />\n ) : null}\n </div>\n </section>\n );\n}\n\nfunction DefaultMarqueeContent() {\n return (\n <>\n <motion.span\n initial={{ opacity: 0, y: 10 }}\n animate={{ opacity: 1, y: 0 }}\n transition={{ delay: 0.05, type: \"spring\", stiffness: 120, damping: 18 }}\n className=\"mb-5 inline-flex items-center gap-2 rounded-full border border-white/15 bg-white/5 px-3 py-1 text-xs uppercase tracking-[0.18em] text-white/70 backdrop-blur\"\n >\n <span className=\"h-1.5 w-1.5 rounded-full bg-white/80\" aria-hidden />\n Trusted across teams\n </motion.span>\n <motion.h1\n initial={{ opacity: 0, y: 18 }}\n animate={{ opacity: 1, y: 0 }}\n transition={{ delay: 0.12, type: \"spring\", stiffness: 110, damping: 18 }}\n className=\"text-balance text-4xl font-semibold tracking-tight sm:text-6xl\"\n >\n The platform shipping teams ship with.\n </motion.h1>\n <motion.p\n initial={{ opacity: 0, y: 18 }}\n animate={{ opacity: 1, y: 0 }}\n transition={{ delay: 0.22, type: \"spring\", stiffness: 110, damping: 18 }}\n className=\"mt-5 max-w-xl text-pretty text-base text-white/65 sm:text-lg\"\n >\n From scrappy seed startups to public companies — UniqueUI components\n ship in production every day.\n </motion.p>\n </>\n );\n}\n",
0 commit comments