{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "logo-cycle",
  "type": "registry:ui",
  "title": "Logo Cycle",
  "description": "A logo row that holds a fixed count and cycles the extras: the current set staggers up and blurs out as the next slides in from below. Hover pauses the loop.",
  "dependencies": [
    "gsap",
    "@gsap/react"
  ],
  "registryDependencies": [],
  "cssVars": {
    "theme": {}
  },
  "files": [
    {
      "path": "components/tweenui/logo-cycle.tsx",
      "type": "registry:component",
      "target": "components/tweenui/logo-cycle.tsx",
      "content": "'use client';\n\nimport { useRef, type ComponentPropsWithoutRef, type ReactNode } from 'react';\nimport { useGSAP } from '@gsap/react';\nimport gsap from 'gsap';\nimport { cn } from '@/lib/utils';\n\ngsap.registerPlugin(useGSAP);\n\nconst prefersReducedMotion = () =>\n  typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n\nconst GROUP_LAYOUT = 'flex flex-wrap items-center justify-center gap-x-8 gap-y-4';\n\nexport interface LogoCycleLogo {\n  src: string;\n  /** Optional mark shown under `.dark`. */\n  srcDark?: string;\n  alt: string;\n}\n\nexport interface LogoCycleProps extends ComponentPropsWithoutRef<'div'> {\n  /** Logos to cycle through in groups. */\n  logos: LogoCycleLogo[];\n  /** How many logos are visible in each group. */\n  visibleCount?: number;\n  /** Seconds between group swaps. */\n  interval?: number;\n  /** Duration of each swap tween, in seconds. */\n  duration?: number;\n  /** Stagger between items in a group, in seconds. */\n  stagger?: number;\n  /** Seconds to wait before the first swap. */\n  delay?: number;\n  /** Vertical travel of the stagger, in pixels. */\n  yOffset?: number;\n  /** Blur radius applied while swapping, in pixels. */\n  blur?: number;\n  /** GSAP ease for the swap. */\n  ease?: string;\n  /** Pause the cycle while the pointer is over the row. */\n  pauseOnHover?: boolean;\n  /** Extra classes for each logo slot. */\n  itemClassName?: string;\n  /** Custom logo markup. Receives the logo and its index in `logos`. */\n  renderLogo?: (logo: LogoCycleLogo, index: number) => ReactNode;\n}\n\nfunction chunkLogos<T>(items: T[], size: number): T[][] {\n  const groups: T[][] = [];\n  for (let i = 0; i < items.length; i += size) {\n    groups.push(items.slice(i, i + size));\n  }\n  return groups;\n}\n\nconst LogoImage = ({ src, srcDark, alt }: LogoCycleLogo) => {\n  if (!srcDark) {\n    return <img src={src} alt={alt} className=\"size-full object-contain\" />;\n  }\n  return (\n    <>\n      <img src={src} alt={alt} className=\"size-full object-contain dark:hidden\" />\n      <img\n        src={srcDark}\n        alt=\"\"\n        aria-hidden=\"true\"\n        className=\"hidden size-full object-contain dark:block\"\n      />\n    </>\n  );\n};\n\nconst LogoMark = ({\n  logo,\n  index,\n  itemClassName,\n  renderLogo,\n  itemRef,\n}: {\n  logo: LogoCycleLogo;\n  index: number;\n  itemClassName?: string;\n  renderLogo?: (logo: LogoCycleLogo, index: number) => ReactNode;\n  itemRef?: (el: HTMLElement | null) => void;\n}) => {\n  return (\n    <figure\n      ref={itemRef}\n      data-logo-cycle-item\n      className={cn('grid h-11 w-12 shrink-0 place-items-center', itemClassName)}\n    >\n      {renderLogo ? (\n        renderLogo(logo, index)\n      ) : (\n        <LogoImage src={logo.src} srcDark={logo.srcDark} alt={logo.alt} />\n      )}\n    </figure>\n  );\n};\n\nconst LogoCycle = ({\n  logos,\n  visibleCount = 6,\n  interval = 2.5,\n  duration = 0.6,\n  stagger = 0.14,\n  delay = 0.5,\n  yOffset = 40,\n  blur = 4,\n  ease = 'power1.inOut',\n  pauseOnHover = true,\n  itemClassName,\n  renderLogo,\n  className,\n  style,\n  ...props\n}: LogoCycleProps) => {\n  const rootRef = useRef<HTMLDivElement>(null);\n  const groupRefs = useRef<(HTMLDivElement | null)[]>([]);\n  const itemRefs = useRef<(HTMLElement | null)[][]>([]);\n\n  const groupSize = Math.max(1, visibleCount);\n  const groups = chunkLogos(logos, groupSize);\n  const isCycling = groups.length > 1;\n\n  useGSAP(\n    () => {\n      const root = rootRef.current;\n      if (!root || !isCycling) return;\n\n      const groupEls = groupRefs.current.filter((el): el is HTMLDivElement => el !== null);\n      const itemGroups = itemRefs.current.map((row) =>\n        (row ?? []).filter((el): el is HTMLElement => el !== null)\n      );\n      const groupCount = Math.min(groupEls.length, itemGroups.length);\n      if (groupCount <= 1) return;\n\n      const blurHidden = `blur(${blur}px)`;\n      const blurClear = 'none';\n\n      for (let i = 0; i < groupCount; i++) {\n        const groupEl = groupEls[i];\n        const groupItems = itemGroups[i];\n        if (!groupEl || !groupItems.length) continue;\n\n        if (i === 0) {\n          gsap.set(groupEl, { autoAlpha: 1, pointerEvents: 'auto' });\n          gsap.set(groupItems, { autoAlpha: 1, y: 0, filter: blurClear });\n        } else {\n          gsap.set(groupEl, { autoAlpha: 0, pointerEvents: 'none' });\n          gsap.set(groupItems, { autoAlpha: 0, y: yOffset, filter: blurHidden });\n        }\n      }\n\n      if (prefersReducedMotion()) return;\n\n      let currentIndex = 0;\n      let loopCall: gsap.core.Tween | undefined;\n      const swapTweens: gsap.core.Tween[] = [];\n\n      const run = () => {\n        const nextIndex = (currentIndex + 1) % groupCount;\n        const current = groupEls[currentIndex];\n        const next = groupEls[nextIndex];\n        const currentItems = itemGroups[currentIndex];\n        const nextItems = itemGroups[nextIndex];\n\n        if (!current || !next || !currentItems?.length || !nextItems?.length) return;\n\n        gsap.set(next, { autoAlpha: 1, pointerEvents: 'auto' });\n\n        swapTweens.length = 0;\n        swapTweens.push(\n          gsap.to(currentItems, {\n            y: -yOffset,\n            autoAlpha: 0,\n            filter: blurHidden,\n            duration,\n            stagger,\n            ease,\n          }),\n          gsap.fromTo(\n            nextItems,\n            { y: yOffset, autoAlpha: 0, filter: blurHidden },\n            {\n              y: 0,\n              autoAlpha: 1,\n              filter: blurClear,\n              duration,\n              stagger,\n              ease,\n              onComplete: () => {\n                gsap.set(current, { autoAlpha: 0, pointerEvents: 'none' });\n              },\n            }\n          )\n        );\n\n        currentIndex = nextIndex;\n      };\n\n      const schedule = (wait: number) => {\n        loopCall = gsap.delayedCall(wait, () => {\n          run();\n          schedule(interval);\n        });\n      };\n\n      schedule(delay);\n\n      if (!pauseOnHover) return;\n\n      const pause = () => {\n        loopCall?.pause();\n        swapTweens.forEach((tween) => tween.pause());\n      };\n      const resume = () => {\n        loopCall?.resume();\n        swapTweens.forEach((tween) => tween.resume());\n      };\n\n      root.addEventListener('pointerenter', pause);\n      root.addEventListener('pointerleave', resume);\n      return () => {\n        root.removeEventListener('pointerenter', pause);\n        root.removeEventListener('pointerleave', resume);\n      };\n    },\n    {\n      scope: rootRef,\n      dependencies: [\n        isCycling,\n        groupSize,\n        interval,\n        duration,\n        stagger,\n        delay,\n        yOffset,\n        blur,\n        ease,\n        pauseOnHover,\n        logos.length,\n      ],\n    }\n  );\n\n  if (!isCycling) {\n    return (\n      <div\n        data-logo-cycle\n        className={cn(GROUP_LAYOUT, 'motion-reduce:transition-none', className)}\n        style={style}\n        {...props}\n      >\n        {logos.map((logo, index) => (\n          <LogoMark\n            key={`${logo.alt}-${index}`}\n            logo={logo}\n            index={index}\n            itemClassName={itemClassName}\n            renderLogo={renderLogo}\n          />\n        ))}\n      </div>\n    );\n  }\n\n  return (\n    <div\n      ref={rootRef}\n      data-logo-cycle\n      className={cn('grid w-full place-items-center motion-reduce:transition-none', className)}\n      style={{ paddingBlock: yOffset, ...style }}\n      {...props}\n    >\n      {groups.map((group, groupIndex) => (\n        <div\n          key={group.map((logo) => logo.alt).join('-') || `logo-group-${groupIndex}`}\n          ref={(el) => {\n            groupRefs.current[groupIndex] = el;\n          }}\n          data-logo-cycle-group\n          className={GROUP_LAYOUT}\n          style={{\n            gridArea: '1 / 1',\n            ...(groupIndex === 0 ? undefined : { opacity: 0, pointerEvents: 'none' }),\n          }}\n        >\n          {group.map((logo, itemIndex) => {\n            const index = groupIndex * groupSize + itemIndex;\n            return (\n              <LogoMark\n                key={`${logo.alt}-${index}`}\n                logo={logo}\n                index={index}\n                itemClassName={itemClassName}\n                renderLogo={renderLogo}\n                itemRef={(el) => {\n                  if (!itemRefs.current[groupIndex]) itemRefs.current[groupIndex] = [];\n                  itemRefs.current[groupIndex][itemIndex] = el;\n                }}\n              />\n            );\n          })}\n        </div>\n      ))}\n    </div>\n  );\n};\n\nexport default LogoCycle;\n"
    }
  ],
  "docs": "Docs, props and examples: https://tween-ui.vercel.app/component/logo-cycle"
}