{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "logos-swap",
  "title": "Logos Swap",
  "author": "ncdai <dai@chanhdai.com>",
  "description": "Swap logos one at a time in a scattered order.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "src/registry/components/logos-swap/logos-swap.tsx",
      "content": "\"use client\"\n\nimport { Children, useEffect, 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, Variants } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst DEFAULT_COLUMN_COUNT = 4\n\nconst SWAP_INTERVAL = 1500\n\nconst GOLDEN_RATIO = 1.618\n\nconst EASE_IN_OUT_EXPO = [0.87, 0, 0.13, 1] as const\n\nconst TRANSITION: Transition = { duration: 0.9, ease: EASE_IN_OUT_EXPO }\n\nconst LOGO_VARIANTS: Variants = {\n  initial: { opacity: 0, transform: \"translateY(50%)\" },\n  animate: {\n    opacity: 1,\n    transform: \"translateY(0%)\",\n    transition: { ...TRANSITION, delay: 0.1 },\n  },\n  exit: {\n    opacity: 0,\n    transform: \"translateY(-50%)\",\n    transition: TRANSITION,\n  },\n}\n\nexport type LogosSwapProps = 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 logos shown at once. Capped at the number of logos. They sit in\n   * one row by default; set `--column-count` to wrap them onto fewer columns.\n   * @defaultValue 4\n   */\n  columnCount?: number\n}\n\nexport function LogosSwap({\n  children,\n  columnCount = DEFAULT_COLUMN_COUNT,\n  className,\n  style,\n  ...props\n}: LogosSwapProps) {\n  const logos = Children.toArray(children)\n  const logoCount = logos.length\n  const slotCount = Math.min(columnCount, logoCount)\n\n  const swapOrder = getSwapOrder(slotCount)\n  // Queued in swap order so the slots start in `children` order. Each slot then\n  // takes the next logo on its turn, which rules out duplicates on screen.\n  const queue = [\n    ...swapOrder.map((slot) => logos[slot]),\n    ...logos.slice(slotCount),\n  ]\n\n  const containerRef = useRef<HTMLDivElement>(null)\n  const isPageInView = usePageInView()\n  const isInView = useInView(containerRef, { margin: \"100px\" })\n  const shouldReduceMotion = useReducedMotion()\n\n  const shouldPlay =\n    isPageInView && isInView && logoCount > slotCount && !shouldReduceMotion\n\n  const [step, setStep] = useState(0)\n\n  useEffect(() => {\n    if (!shouldPlay) return\n\n    const intervalId = window.setInterval(\n      () => setStep((prev) => prev + 1),\n      SWAP_INTERVAL\n    )\n\n    return () => window.clearInterval(intervalId)\n  }, [shouldPlay])\n\n  if (logoCount === 0) return null\n\n  return (\n    <div\n      ref={containerRef}\n      data-slot=\"logos-swap\"\n      className={cn(\"grid\", className)}\n      style={{\n        gridTemplateColumns: `repeat(var(--column-count,${slotCount}), minmax(0, 1fr))`,\n        ...style,\n      }}\n      {...props}\n    >\n      {Array.from({ length: slotCount }, (_, slot) => {\n        const turn = swapOrder.indexOf(slot)\n        const swapCount = Math.ceil((step - turn) / slotCount)\n        const queueIndex = (turn + swapCount * slotCount) % logoCount\n\n        return (\n          // The bleed holds the feathered edge, so logos at rest are never dimmed.\n          <div\n            key={slot}\n            data-slot=\"logos-swap-slot\"\n            className=\"-my-3 grid h-20 min-w-0 grid-cols-1 grid-rows-1 overflow-hidden mask-y-from-85% py-3\"\n          >\n            <AnimatePresence initial={false}>\n              <motion.div\n                key={queueIndex}\n                data-slot=\"logos-swap-logo\"\n                className=\"col-start-1 row-start-1 flex min-h-0 min-w-0 items-center justify-center\"\n                variants={LOGO_VARIANTS}\n                initial=\"initial\"\n                animate=\"animate\"\n                exit=\"exit\"\n              >\n                {queue[queueIndex]}\n              </motion.div>\n            </AnimatePresence>\n          </div>\n        )\n      })}\n    </div>\n  )\n}\n\n/** Golden ratio steps scatter the slots like a shuffle the server can match. */\nfunction getSwapOrder(slotCount: number) {\n  const getRank = (slot: number) => (slot * GOLDEN_RATIO) % 1\n\n  return Array.from({ length: slotCount }, (_, slot) => slot).sort(\n    (a, b) => getRank(a) - getRank(b)\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/logos-swap.tsx"
    }
  ],
  "docs": "https://chanhdai.com/components/logos-swap",
  "categories": [
    "marketing"
  ],
  "type": "registry:component"
}