{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "logos-carousel",
  "title": "Logos Carousel",
  "author": "ncdai <dai@chanhdai.com>",
  "description": "Cycle through logos column by column in a staggered wave.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "src/registry/components/logos-carousel/logos-carousel.tsx",
      "content": "\"use client\"\n\nimport { Children, memo, useEffect, useMemo, useRef, useState } from \"react\"\nimport type { ReactNode } from \"react\"\nimport {\n  AnimatePresence,\n  motion,\n  useInView,\n  usePageInView,\n  useReducedMotion,\n} from \"motion/react\"\nimport type { Transition } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst DEFAULT_COLUMN_COUNT = 4\n\n/** How long each logo stays visible before cycling to the next one (ms). */\nconst CYCLE_INTERVAL = 2400\n\n/**\n * Delay between adjacent columns within a single wave (ms). Kept smaller than\n * the enter/exit duration so neighbouring transitions overlap into a ripple.\n */\nconst STAGGER_DELAY = 125\n\n/** Duration of a single enter or exit transition (s). */\nconst TRANSITION_DURATION = 0.5\n\nconst EASE_OUT_QUAD = [0.25, 0.46, 0.45, 0.94] as const\n\n/** Direction the wave sweeps across the columns. */\ntype WaveDirection = \"ltr\" | \"rtl\"\n\nexport type LogosCarouselProps = {\n  /** Logo elements to cycle through. Each child is rendered as a single logo. */\n  children: ReactNode\n  /**\n   * Number of columns to spread the logos across. Capped at the number of logos.\n   * @defaultValue 4\n   */\n  columnCount?: number\n  /**\n   * Direction the ripple travels: left-to-right or right-to-left.\n   * @defaultValue \"ltr\"\n   */\n  direction?: WaveDirection\n  className?: string\n}\n\nexport function LogosCarousel({\n  children,\n  columnCount = DEFAULT_COLUMN_COUNT,\n  direction = \"ltr\",\n  className,\n}: LogosCarouselProps) {\n  const columns = useMemo(\n    () => distributeLogos(Children.toArray(children), columnCount),\n    [children, columnCount]\n  )\n\n  const reduceMotion = useReducedMotion() ?? false\n\n  const containerRef = useRef<HTMLDivElement>(null)\n  const isPageInView = usePageInView()\n  const isInView = useInView(containerRef, { margin: \"100px\" })\n\n  const [step, setStep] = useState(0)\n  const shouldPlay = isPageInView && isInView\n\n  useEffect(() => {\n    if (!shouldPlay) return\n\n    const beatId = setInterval(\n      () => setStep((prev) => prev + 1),\n      CYCLE_INTERVAL\n    )\n\n    return () => clearInterval(beatId)\n  }, [shouldPlay])\n\n  return (\n    <div\n      ref={containerRef}\n      data-slot=\"logos-carousel\"\n      className={cn(\"grid\", className)}\n      style={{\n        gridTemplateColumns: `repeat(var(--column-count,${columns.length}), minmax(0, 1fr))`,\n      }}\n    >\n      {columns.map((columnLogos, columnIndex) => {\n        const waveIndex =\n          direction === \"rtl\" ? columns.length - 1 - columnIndex : columnIndex\n\n        return (\n          <LogoColumn\n            key={columnIndex}\n            logos={columnLogos}\n            columnIndex={columnIndex}\n            waveIndex={waveIndex}\n            activeIndex={step % columnLogos.length}\n            reduceMotion={reduceMotion}\n          />\n        )\n      })}\n    </div>\n  )\n}\n\ntype LogoColumnProps = {\n  logos: ReactNode[]\n  columnIndex: number\n  waveIndex: number\n  activeIndex: number\n  reduceMotion: boolean\n}\n\nconst LogoColumn = memo(function LogoColumn({\n  logos,\n  columnIndex,\n  waveIndex,\n  activeIndex,\n  reduceMotion,\n}: LogoColumnProps) {\n  const transition: Transition = {\n    ease: EASE_OUT_QUAD,\n    duration: TRANSITION_DURATION,\n    delay: waveIndex * (STAGGER_DELAY / 1000),\n  }\n\n  const column = getColumnVariants(reduceMotion)\n  const logo = getLogoVariants(reduceMotion, transition)\n\n  return (\n    <motion.div\n      data-slot=\"logos-carousel-column\"\n      className=\"relative\"\n      initial={column.initial}\n      animate={column.animate}\n      transition={transition}\n    >\n      <AnimatePresence mode=\"popLayout\">\n        <motion.div\n          key={`${columnIndex}-${activeIndex}`}\n          data-slot=\"logos-carousel-logo\"\n          className=\"flex size-full items-center justify-center\"\n          initial={logo.initial}\n          animate={logo.animate}\n          exit={logo.exit}\n        >\n          {logos[activeIndex]}\n        </motion.div>\n      </AnimatePresence>\n    </motion.div>\n  )\n})\n\n/**\n * Under reduced motion every variant collapses to a plain opacity cross-fade.\n * The logos still cycle, so no logo is dropped from the run; only the travel\n * and blur that can trigger vestibular discomfort are removed.\n */\nfunction getColumnVariants(reduceMotion: boolean) {\n  if (reduceMotion) {\n    return {\n      initial: { opacity: 0 },\n      animate: { opacity: 1 },\n    }\n  }\n\n  return {\n    initial: { opacity: 0, transform: \"translateY(60%)\" },\n    animate: { opacity: 1, transform: \"translateY(0%)\" },\n  }\n}\n\nfunction getLogoVariants(reduceMotion: boolean, transition: Transition) {\n  if (reduceMotion) {\n    return {\n      initial: { opacity: 0 },\n      animate: { opacity: 1, transition },\n      exit: { opacity: 0, transition },\n    }\n  }\n\n  return {\n    initial: { opacity: 0, transform: \"translateY(60%)\", filter: \"blur(2px)\" },\n    animate: {\n      opacity: 1,\n      transform: \"translateY(0%)\",\n      filter: \"blur(0px)\",\n      transition,\n    },\n    exit: {\n      opacity: 0,\n      transform: \"translateY(-50%)\",\n      filter: \"blur(3px)\",\n      transition,\n    },\n  }\n}\n\nfunction distributeLogos(\n  logos: ReactNode[],\n  columnCount: number\n): ReactNode[][] {\n  const effectiveCount = Math.min(columnCount, logos.length)\n  const columns: ReactNode[][] = Array.from(\n    { length: effectiveCount },\n    () => []\n  )\n\n  logos.forEach((logo, index) => {\n    columns[index % effectiveCount].push(logo)\n  })\n\n  return columns\n}\n",
      "type": "registry:component",
      "target": "@components/logos-carousel.tsx"
    }
  ],
  "docs": "https://chanhdai.com/components/logos-carousel",
  "categories": [
    "marketing"
  ],
  "type": "registry:component"
}