{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "logo-wave",
  "type": "registry:ui",
  "title": "Logo Wave",
  "description": "An endless logo strip where the mark nearest the midpoint rises and scales up while its neighbors step down into a wave. Hover pauses the marquee.",
  "dependencies": [
    "gsap",
    "@gsap/react"
  ],
  "registryDependencies": [],
  "cssVars": {
    "theme": {}
  },
  "files": [
    {
      "path": "components/tweenui/logo-wave.tsx",
      "type": "registry:component",
      "target": "components/tweenui/logo-wave.tsx",
      "content": "'use client';\n\nimport {\n  useLayoutEffect,\n  useRef,\n  useState,\n  type ComponentPropsWithoutRef,\n  type ReactNode,\n} 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 LogoImage = ({ src, srcDark, alt }: LogoWaveLogo) => {\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 ITEM_SIZE = 42;\n\nexport interface LogoWaveLogo {\n  src: string;\n  /** Optional mark shown under `.dark`. */\n  srcDark?: string;\n  alt: string;\n}\n\nexport interface LogoWaveStep {\n  /** Scale of the mark at this distance from center. */\n  scale: number;\n  /** Vertical offset in pixels (negative = up). */\n  y: number;\n}\n\nexport const DEFAULT_LOGO_WAVE: LogoWaveStep[] = [\n  { scale: 2.6, y: -110 },\n  { scale: 1.8, y: -70 },\n  { scale: 1.5, y: -50 },\n  { scale: 1.3, y: -35 },\n  { scale: 1.2, y: -20 },\n];\n\nexport interface LogoWaveProps extends ComponentPropsWithoutRef<'div'> {\n  /** Logos duplicated along the marquee. */\n  logos: LogoWaveLogo[];\n  /** Seconds for one set to travel its own width. */\n  duration?: number;\n  /** Space between items, in pixels. */\n  gap?: number;\n  /** Scroll direction. `left` is right-to-left; `right` is left-to-right. */\n  direction?: 'left' | 'right';\n  /** Pause the marquee while the pointer is over it. */\n  pauseOnHover?: boolean;\n  /** Extra classes for each logo chip. */\n  itemClassName?: string;\n  /** Custom logo markup. Receives the logo and its index in `logos`. */\n  renderLogo?: (logo: LogoWaveLogo, index: number) => ReactNode;\n  /** Wave steps from the center mark outward. Index 0 is the peak. */\n  wave?: LogoWaveStep[];\n}\n\nfunction peakPadding(wave: LogoWaveStep[]) {\n  const peak = wave[0] ?? DEFAULT_LOGO_WAVE[0];\n  const extra = (ITEM_SIZE * peak.scale - ITEM_SIZE) / 2;\n  return {\n    paddingTop: Math.abs(peak.y) + extra,\n    paddingBottom: extra,\n  };\n}\n\nfunction applyWave(items: HTMLElement[], center: HTMLElement, steps: LogoWaveStep[]) {\n  const centerIdx = items.indexOf(center);\n  items.forEach((el, i) => {\n    const dist = Math.abs(i - centerIdx);\n    const step = steps[dist];\n    if (step) {\n      el.style.transform = `translateY(${step.y}px) scale(${step.scale})`;\n      el.style.zIndex = String(steps.length - dist);\n    } else {\n      el.style.transform = '';\n      el.style.zIndex = '';\n    }\n  });\n}\n\nfunction findCenterIcon(container: HTMLElement, track: HTMLElement): HTMLElement | null {\n  const viewportCenterX = container.getBoundingClientRect().left + container.offsetWidth / 2;\n  const icons = track.querySelectorAll<HTMLElement>('[data-logo-wave-item]');\n  let centerIcon: HTMLElement | null = null;\n  let closest = Infinity;\n\n  for (const icon of icons) {\n    const rect = icon.getBoundingClientRect();\n    const distance = Math.abs(rect.left + rect.width / 2 - viewportCenterX);\n    if (distance < closest) {\n      closest = distance;\n      centerIcon = icon;\n    }\n  }\n\n  return centerIcon;\n}\n\nconst LogoWave = ({\n  logos,\n  duration = 60,\n  gap = 70,\n  direction = 'left',\n  pauseOnHover = true,\n  itemClassName,\n  renderLogo,\n  wave = DEFAULT_LOGO_WAVE,\n  className,\n  style,\n  ...props\n}: LogoWaveProps) => {\n  const rootRef = useRef<HTMLDivElement>(null);\n  const trackRef = useRef<HTMLDivElement>(null);\n  const [repeatCount, setRepeatCount] = useState(2);\n  const padding = peakPadding(wave);\n\n  useLayoutEffect(() => {\n    const track = trackRef.current;\n    const root = rootRef.current;\n    if (!track || !root || logos.length === 0) return;\n\n    const updateRepeatCount = () => {\n      const cycleWidth = track.scrollWidth / repeatCount;\n      if (!cycleWidth) return;\n\n      const viewport = root.offsetWidth || window.innerWidth;\n      let needed = 2;\n      while (needed * cycleWidth < viewport + cycleWidth) {\n        needed += 1;\n      }\n\n      setRepeatCount((prev) => (prev === needed ? prev : needed));\n    };\n\n    updateRepeatCount();\n    window.addEventListener('resize', updateRepeatCount);\n    return () => window.removeEventListener('resize', updateRepeatCount);\n  }, [repeatCount, logos.length, gap]);\n\n  useGSAP(\n    () => {\n      const container = rootRef.current;\n      const track = trackRef.current;\n      if (!container || !track || logos.length === 0) return;\n\n      const items = Array.from(track.querySelectorAll<HTMLElement>('[data-logo-wave-item]'));\n      if (items.length === 0) return;\n\n      const syncActive = () => {\n        const center = findCenterIcon(container, track);\n        if (!center) return;\n        applyWave(items, center, wave);\n      };\n\n      syncActive();\n\n      if (prefersReducedMotion()) return;\n\n      const cycleWidth = track.scrollWidth / repeatCount || 1;\n      const toRight = direction === 'right';\n\n      gsap.set(track, { x: toRight ? -cycleWidth : 0 });\n\n      let lastCenter: HTMLElement | null = null;\n      const tween = gsap.to(track, {\n        x: toRight ? 0 : -cycleWidth,\n        duration,\n        ease: 'none',\n        repeat: -1,\n        onUpdate: () => {\n          const center = findCenterIcon(container, track);\n          if (!center || center === lastCenter) return;\n          lastCenter = center;\n          applyWave(items, center, wave);\n        },\n      });\n\n      if (!pauseOnHover) return;\n\n      const pause = () => tween.pause();\n      const resume = () => tween.resume();\n      container.addEventListener('pointerenter', pause);\n      container.addEventListener('pointerleave', resume);\n      return () => {\n        container.removeEventListener('pointerenter', pause);\n        container.removeEventListener('pointerleave', resume);\n      };\n    },\n    {\n      scope: rootRef,\n      dependencies: [repeatCount, duration, direction, pauseOnHover, logos.length, gap, wave],\n    }\n  );\n\n  return (\n    <div\n      ref={rootRef}\n      data-logo-wave\n      data-direction={direction}\n      className={cn('relative w-full overflow-hidden motion-reduce:transition-none', className)}\n      style={{ ...padding, ...style }}\n      {...props}\n    >\n      <div\n        ref={trackRef}\n        data-logo-wave-track\n        className=\"flex w-max shrink-0 items-end will-change-transform\"\n        style={{ columnGap: gap }}\n      >\n        {Array.from({ length: repeatCount }, (_, setIndex) =>\n          logos.map((logo, index) => (\n            <figure\n              key={`${setIndex}-${logo.alt}-${index}`}\n              data-logo-wave-item\n              className={cn(\n                'grid size-[42px] shrink-0 place-items-center rounded-full border border-[#045f64]/10 bg-white p-2 shadow-[0_1px_2px_rgba(16,24,40,0.12)] transition-transform duration-500 ease-in-out motion-reduce:transition-none dark:border-[#045f64]/40 dark:bg-[#12161F]',\n                itemClassName\n              )}\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      </div>\n    </div>\n  );\n};\n\nexport default LogoWave;\n"
    }
  ],
  "docs": "Docs, props and examples: https://tween-ui.vercel.app/component/logo-wave"
}