{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "logos-flip",
  "title": "Logos Flip",
  "author": "ncdai <dai@chanhdai.com>",
  "description": "Flip cards in a staggered wave to reveal the next batch of logos.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "src/registry/components/logos-flip/logos-flip.tsx",
      "content": "\"use client\"\n\nimport { Children, useEffect, useRef, useState } from \"react\"\nimport type { ReactNode } from \"react\"\nimport { useInView, usePageInView } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst DEFAULT_COLUMN_COUNT = 4\n\n/** How long each batch of logos stays up before the next wave (ms). */\nconst CYCLE_INTERVAL = 3500\n\n/** Delay between adjacent cards inside one wave (ms). */\nconst STAGGER_DELAY = 160\n\n/** Duration of a single card flip (ms). Must match the Tailwind duration below. */\nconst FLIP_DURATION = 600\n\n/** Slack after the last card settles before the faces are swapped (ms). */\nconst RESET_BUFFER = 50\n\nexport type LogosFlipProps = Omit<React.ComponentProps<\"div\">, \"ref\"> & {\n  /** Logo elements to cycle through. Each child is rendered as a single logo. */\n  children: ReactNode\n  /**\n   * Number of cards shown at once. Capped at the number of logos. The cards\n   * sit in one row by default; set `--column-count` to wrap them onto fewer\n   * columns.\n   * @defaultValue 4\n   */\n  columnCount?: number\n}\n\nexport function LogosFlip({\n  children,\n  columnCount = DEFAULT_COLUMN_COUNT,\n  className,\n  style,\n  ...props\n}: LogosFlipProps) {\n  const logos = Children.toArray(children)\n  const logoCount = logos.length\n  const cardCount = Math.min(columnCount, logoCount)\n\n  const containerRef = useRef<HTMLDivElement>(null)\n  const isPageInView = usePageInView()\n  const isInView = useInView(containerRef, { margin: \"100px\" })\n\n  const shouldPlay = isPageInView && isInView && logoCount > cardCount\n\n  const [offset, setOffset] = useState(0)\n  const [flippedCount, setFlippedCount] = useState(0)\n\n  useEffect(() => {\n    if (!shouldPlay) return\n\n    const timeouts = new Set<number>()\n    let isAnimating = false\n\n    const schedule = (callback: () => void, delay: number) => {\n      const id = window.setTimeout(() => {\n        timeouts.delete(id)\n        callback()\n      }, delay)\n      timeouts.add(id)\n    }\n\n    const settleWave = () => {\n      setOffset((prev) => prev + cardCount)\n      setFlippedCount(0)\n      isAnimating = false\n    }\n\n    const runWave = () => {\n      if (isAnimating) return\n      isAnimating = true\n\n      for (let slot = 0; slot < cardCount; slot++) {\n        schedule(() => setFlippedCount(slot + 1), slot * STAGGER_DELAY)\n      }\n\n      schedule(\n        settleWave,\n        (cardCount - 1) * STAGGER_DELAY + FLIP_DURATION + RESET_BUFFER\n      )\n    }\n\n    const intervalId = window.setInterval(runWave, CYCLE_INTERVAL)\n\n    return () => {\n      window.clearInterval(intervalId)\n      timeouts.forEach((id) => window.clearTimeout(id))\n      // The grid is off screen or the tab is hidden here, so committing an\n      // interrupted wave is invisible and playback resumes from its batch.\n      if (isAnimating) settleWave()\n    }\n  }, [shouldPlay, cardCount])\n\n  if (logoCount === 0) return null\n\n  return (\n    <div\n      ref={containerRef}\n      data-slot=\"logos-flip\"\n      className={cn(\"grid gap-2\", className)}\n      style={{\n        gridTemplateColumns: `repeat(var(--column-count,${cardCount}), minmax(0, 1fr))`,\n        ...style,\n      }}\n      {...props}\n    >\n      {Array.from({ length: cardCount }, (_, slot) => (\n        <LogosFlipCard\n          key={slot}\n          offset={offset}\n          front={logos[(slot + offset) % logoCount]}\n          back={logos[(slot + offset + cardCount) % logoCount]}\n          flipped={slot < flippedCount}\n        />\n      ))}\n    </div>\n  )\n}\n\ntype LogosFlipCardProps = {\n  offset: number\n  front: ReactNode\n  back: ReactNode\n  flipped: boolean\n}\n\nfunction LogosFlipCard({ offset, front, back, flipped }: LogosFlipCardProps) {\n  return (\n    <div\n      data-slot=\"logos-flip-card\"\n      className=\"h-20 min-w-0 perspective-[1000px] sm:h-24\"\n    >\n      {/* Remounting on swap returns the card to 0deg without a transition. */}\n      <div\n        key={offset}\n        data-slot=\"logos-flip-card-inner\"\n        className={cn(\n          \"relative size-full transition-transform duration-600 ease-in-out transform-3d motion-reduce:transition-none\",\n          flipped ? \"rotate-x-180 motion-reduce:rotate-x-0\" : \"rotate-x-0\"\n        )}\n      >\n        <LogosFlipFace className=\"rotate-x-0\">{front}</LogosFlipFace>\n        <LogosFlipFace className=\"rotate-x-180\">{back}</LogosFlipFace>\n      </div>\n    </div>\n  )\n}\n\nfunction LogosFlipFace({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"logos-flip-face\"\n      className={cn(\n        \"absolute inset-0 flex items-center justify-center rounded-lg border bg-card px-2 backface-hidden\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/logos-flip.tsx"
    }
  ],
  "docs": "https://chanhdai.com/components/logos-flip",
  "categories": [
    "marketing"
  ],
  "type": "registry:component"
}