{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "logo-loop",
  "title": "Logo Timeline",
  "description": "A looping logo marquee component with horizontal and vertical motion, hover effects, fading edges, and responsive behavior.",
  "dependencies": [
    "react"
  ],
  "files": [
    {
      "path": "registry/gammaui/logo-loop.tsx",
      "content": "\"use client\"\n\nimport React, { useCallback, useEffect, useMemo, useRef, useState } from \"react\"\n\nexport type LogoItem =\n  | {\n      node: React.ReactNode\n      href?: string\n      title?: string\n      ariaLabel?: string\n    }\n  | {\n      src: string\n      alt?: string\n      href?: string\n      title?: string\n      srcSet?: string\n      sizes?: string\n      width?: number\n      height?: number\n    }\n\nexport interface LogoLoopProps {\n  logos: LogoItem[]\n  speed?: number\n  direction?: \"left\" | \"right\" | \"up\" | \"down\"\n  width?: number | string\n  logoHeight?: number\n  gap?: number\n  pauseOnHover?: boolean\n  hoverSpeed?: number\n  fadeOut?: boolean\n  fadeOutColor?: string\n  scaleOnHover?: boolean\n  renderItem?: (item: LogoItem, key: React.Key) => React.ReactNode\n  ariaLabel?: string\n  className?: string\n  style?: React.CSSProperties\n}\n\nconst ANIMATION_CONFIG = {\n  SMOOTH_TAU: 0.25,\n  MIN_COPIES: 2,\n  COPY_HEADROOM: 2,\n} as const\n\nconst toCssLength = (value?: number | string): string | undefined =>\n  typeof value === \"number\" ? `${value}px` : (value ?? undefined)\n\nconst cx = (...parts: (string | false | null | undefined)[]) =>\n  parts.filter(Boolean).join(\" \")\n\nconst useResizeObserver = (\n  callback: () => void,\n  elements: React.RefObject<Element | null>[],\n  dependencies: React.DependencyList\n) => {\n  useEffect(() => {\n    if (!window.ResizeObserver) {\n      const handleResize = () => callback()\n      window.addEventListener(\"resize\", handleResize)\n      callback()\n      return () => window.removeEventListener(\"resize\", handleResize)\n    }\n\n    const observers = elements.map((ref) => {\n      if (!ref.current) return null\n      const observer = new ResizeObserver(callback)\n      observer.observe(ref.current)\n      return observer\n    })\n\n    callback()\n\n    return () => {\n      observers.forEach((observer) => observer?.disconnect())\n    }\n  }, dependencies)\n}\n\nconst useImageLoader = (\n  seqRef: React.RefObject<HTMLUListElement | null>,\n  onLoad: () => void,\n  dependencies: React.DependencyList\n) => {\n  useEffect(() => {\n    const images = seqRef.current?.querySelectorAll(\"img\") ?? []\n\n    if (images.length === 0) {\n      onLoad()\n      return\n    }\n\n    let remainingImages = images.length\n    const handleImageLoad = () => {\n      remainingImages -= 1\n      if (remainingImages === 0) {\n        onLoad()\n      }\n    }\n\n    images.forEach((img) => {\n      const htmlImg = img as HTMLImageElement\n      if (htmlImg.complete) {\n        handleImageLoad()\n      } else {\n        htmlImg.addEventListener(\"load\", handleImageLoad, { once: true })\n        htmlImg.addEventListener(\"error\", handleImageLoad, { once: true })\n      }\n    })\n\n    return () => {\n      images.forEach((img) => {\n        img.removeEventListener(\"load\", handleImageLoad)\n        img.removeEventListener(\"error\", handleImageLoad)\n      })\n    }\n  }, dependencies)\n}\n\nconst useAnimationLoop = (\n  trackRef: React.RefObject<HTMLDivElement | null>,\n  targetVelocity: number,\n  seqWidth: number,\n  seqHeight: number,\n  isHovered: boolean,\n  hoverSpeed: number | undefined,\n  isVertical: boolean\n) => {\n  const rafRef = useRef<number | null>(null)\n  const lastTimestampRef = useRef<number | null>(null)\n  const offsetRef = useRef(0)\n  const velocityRef = useRef(0)\n\n  useEffect(() => {\n    const track = trackRef.current\n    if (!track) return\n\n    const prefersReduced =\n      typeof window !== \"undefined\" &&\n      window.matchMedia &&\n      window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n\n    const seqSize = isVertical ? seqHeight : seqWidth\n\n    if (seqSize > 0) {\n      offsetRef.current = ((offsetRef.current % seqSize) + seqSize) % seqSize\n      const transformValue = isVertical\n        ? `translate3d(0, ${-offsetRef.current}px, 0)`\n        : `translate3d(${-offsetRef.current}px, 0, 0)`\n      track.style.transform = transformValue\n    }\n\n    if (prefersReduced) {\n      track.style.transform = isVertical\n        ? \"translate3d(0, 0, 0)\"\n        : \"translate3d(0, 0, 0)\"\n      return () => {\n        lastTimestampRef.current = null\n      }\n    }\n\n    const animate = (timestamp: number) => {\n      if (lastTimestampRef.current === null) {\n        lastTimestampRef.current = timestamp\n      }\n\n      const deltaTime = Math.max(0, timestamp - lastTimestampRef.current) / 1000\n      lastTimestampRef.current = timestamp\n\n      const target =\n        isHovered && hoverSpeed !== undefined ? hoverSpeed : targetVelocity\n\n      const easingFactor =\n        1 - Math.exp(-deltaTime / ANIMATION_CONFIG.SMOOTH_TAU)\n      velocityRef.current += (target - velocityRef.current) * easingFactor\n\n      if (seqSize > 0) {\n        let nextOffset = offsetRef.current + velocityRef.current * deltaTime\n        nextOffset = ((nextOffset % seqSize) + seqSize) % seqSize\n        offsetRef.current = nextOffset\n\n        const transformValue = isVertical\n          ? `translate3d(0, ${-offsetRef.current}px, 0)`\n          : `translate3d(${-offsetRef.current}px, 0, 0)`\n        track.style.transform = transformValue\n      }\n\n      rafRef.current = requestAnimationFrame(animate)\n    }\n\n    rafRef.current = requestAnimationFrame(animate)\n\n    return () => {\n      if (rafRef.current !== null) {\n        cancelAnimationFrame(rafRef.current)\n        rafRef.current = null\n      }\n      lastTimestampRef.current = null\n    }\n  }, [targetVelocity, seqWidth, seqHeight, isHovered, hoverSpeed, isVertical])\n}\n\nexport const LogoLoop = React.memo<LogoLoopProps>(\n  ({\n    logos,\n    speed = 120,\n    direction = \"left\",\n    width = \"100%\",\n    logoHeight = 28,\n    gap = 32,\n    pauseOnHover,\n    hoverSpeed,\n    fadeOut = false,\n    fadeOutColor,\n    scaleOnHover = false,\n    renderItem,\n    ariaLabel = \"Partner logos\",\n    className,\n    style,\n  }) => {\n    const containerRef = useRef<HTMLDivElement>(null)\n    const trackRef = useRef<HTMLDivElement>(null)\n    const seqRef = useRef<HTMLUListElement>(null)\n\n    const [seqWidth, setSeqWidth] = useState<number>(0)\n    const [seqHeight, setSeqHeight] = useState<number>(0)\n    const [copyCount, setCopyCount] = useState<number>(\n      ANIMATION_CONFIG.MIN_COPIES\n    )\n    const [isHovered, setIsHovered] = useState<boolean>(false)\n\n    const effectiveHoverSpeed = useMemo(() => {\n      if (hoverSpeed !== undefined) return hoverSpeed\n      if (pauseOnHover === true) return 0\n      if (pauseOnHover === false) return undefined\n      return 0\n    }, [hoverSpeed, pauseOnHover])\n\n    const isVertical = direction === \"up\" || direction === \"down\"\n\n    const targetVelocity = useMemo(() => {\n      const magnitude = Math.abs(speed)\n      let directionMultiplier: number\n      if (isVertical) {\n        directionMultiplier = direction === \"up\" ? 1 : -1\n      } else {\n        directionMultiplier = direction === \"left\" ? 1 : -1\n      }\n      const speedMultiplier = speed < 0 ? -1 : 1\n      return magnitude * directionMultiplier * speedMultiplier\n    }, [speed, direction, isVertical])\n\n    const updateDimensions = useCallback(() => {\n      const containerWidth = containerRef.current?.clientWidth ?? 0\n      const sequenceRect = seqRef.current?.getBoundingClientRect?.()\n      const sequenceWidth = sequenceRect?.width ?? 0\n      const sequenceHeight = sequenceRect?.height ?? 0\n      if (isVertical) {\n        const parentHeight =\n          containerRef.current?.parentElement?.clientHeight ?? 0\n        if (containerRef.current && parentHeight > 0) {\n          const targetHeight = Math.ceil(parentHeight)\n          if (containerRef.current.style.height !== `${targetHeight}px`)\n            containerRef.current.style.height = `${targetHeight}px`\n        }\n        if (sequenceHeight > 0) {\n          setSeqHeight(Math.ceil(sequenceHeight))\n          const viewport =\n            containerRef.current?.clientHeight ?? parentHeight ?? sequenceHeight\n          const copiesNeeded =\n            Math.ceil(viewport / sequenceHeight) +\n            ANIMATION_CONFIG.COPY_HEADROOM\n          setCopyCount(Math.max(ANIMATION_CONFIG.MIN_COPIES, copiesNeeded))\n        }\n      } else if (sequenceWidth > 0) {\n        setSeqWidth(Math.ceil(sequenceWidth))\n        const copiesNeeded =\n          Math.ceil(containerWidth / sequenceWidth) +\n          ANIMATION_CONFIG.COPY_HEADROOM\n        setCopyCount(Math.max(ANIMATION_CONFIG.MIN_COPIES, copiesNeeded))\n      }\n    }, [isVertical])\n\n    useResizeObserver(\n      updateDimensions,\n      [containerRef, seqRef],\n      [logos, gap, logoHeight, isVertical]\n    )\n\n    useImageLoader(seqRef, updateDimensions, [\n      logos,\n      gap,\n      logoHeight,\n      isVertical,\n    ])\n\n    useAnimationLoop(\n      trackRef,\n      targetVelocity,\n      seqWidth,\n      seqHeight,\n      isHovered,\n      effectiveHoverSpeed,\n      isVertical\n    )\n\n    const cssVariables = useMemo(\n      () =>\n        ({\n          \"--logoloop-gap\": `${gap}px`,\n          \"--logoloop-logoHeight\": `${logoHeight}px`,\n          ...(fadeOutColor && { \"--logoloop-fadeColor\": fadeOutColor }),\n        }) as React.CSSProperties,\n      [gap, logoHeight, fadeOutColor]\n    )\n\n    const rootClasses = useMemo(\n      () =>\n        cx(\n          \"relative group\",\n          isVertical\n            ? \"overflow-hidden h-full inline-block\"\n            : \"overflow-x-hidden\",\n          \"[--logoloop-gap:32px]\",\n          \"[--logoloop-logoHeight:28px]\",\n          \"[--logoloop-fadeColorAuto:#ffffff]\",\n          \"dark:[--logoloop-fadeColorAuto:#0b0b0b]\",\n          scaleOnHover && \"py-[calc(var(--logoloop-logoHeight)*0.1)]\",\n          className\n        ),\n      [isVertical, scaleOnHover, className]\n    )\n\n    const handleMouseEnter = useCallback(() => {\n      if (effectiveHoverSpeed !== undefined) setIsHovered(true)\n    }, [effectiveHoverSpeed])\n    const handleMouseLeave = useCallback(() => {\n      if (effectiveHoverSpeed !== undefined) setIsHovered(false)\n    }, [effectiveHoverSpeed])\n\n    const renderLogoItem = useCallback(\n      (item: LogoItem, key: React.Key) => {\n        if (renderItem) {\n          return (\n            <li\n              className={cx(\n                \"flex-none text-(length:--logoloop-logoHeight) leading-none\",\n                isVertical ? \"mb-(--logoloop-gap)\" : \"mr-(--logoloop-gap)\",\n                scaleOnHover && \"group/item overflow-visible\"\n              )}\n              key={key}\n              role=\"listitem\"\n            >\n              {renderItem(item, key)}\n            </li>\n          )\n        }\n\n        const isNodeItem = \"node\" in item\n\n        const content = isNodeItem ? (\n          <span\n            className={cx(\n              \"inline-flex items-center\",\n              \"motion-reduce:transition-none\",\n              scaleOnHover &&\n                \"transition-transform duration-300 ease-in-out group-hover/item:scale-120\"\n            )}\n            aria-hidden={!!(item as any).href && !(item as any).ariaLabel}\n          >\n            {(item as any).node}\n          </span>\n        ) : (\n          <img\n            className={cx(\n              \"block h-[--logoloop-logoHeight] w-auto object-contain\",\n              \"pointer-events-none [-webkit-user-drag:none]\",\n              \"[image-rendering:-webkit-optimize-contrast]\",\n              \"motion-reduce:transition-none\",\n              scaleOnHover &&\n                \"transition-transform duration-300 ease-in-out group-hover/item:scale-120\"\n            )}\n            src={(item as any).src}\n            srcSet={(item as any).srcSet}\n            sizes={(item as any).sizes}\n            width={(item as any).width}\n            height={(item as any).height}\n            alt={(item as any).alt ?? \"\"}\n            title={(item as any).title}\n            loading=\"lazy\"\n            decoding=\"async\"\n            draggable={false}\n          />\n        )\n\n        const itemAriaLabel = isNodeItem\n          ? ((item as any).ariaLabel ?? (item as any).title)\n          : ((item as any).alt ?? (item as any).title)\n\n        const inner = (item as any).href ? (\n          <a\n            className={cx(\n              \"inline-flex items-center rounded no-underline\",\n              \"transition-opacity duration-200 ease-linear\",\n              \"hover:opacity-80\",\n              \"focus-visible:outline focus-visible:outline-offset-2 focus-visible:outline-current\"\n            )}\n            href={(item as any).href}\n            aria-label={itemAriaLabel || \"logo link\"}\n            target=\"_blank\"\n            rel=\"noreferrer noopener\"\n          >\n            {content}\n          </a>\n        ) : (\n          content\n        )\n\n        return (\n          <li\n            className={cx(\n              \"flex-none text-(length:--logoloop-logoHeight) leading-none\",\n              isVertical ? \"mb-[--logoloop-gap]\" : \"mr-[--logoloop-gap]\",\n              scaleOnHover && \"group/item overflow-visible\"\n            )}\n            key={key}\n            role=\"listitem\"\n          >\n            {inner}\n          </li>\n        )\n      },\n      [isVertical, scaleOnHover, renderItem]\n    )\n\n    const logoLists = useMemo(\n      () =>\n        Array.from({ length: copyCount }, (_, copyIndex) => (\n          <ul\n            className={cx(\"flex items-center\", isVertical && \"flex-col\")}\n            key={`copy-${copyIndex}`}\n            role=\"list\"\n            aria-hidden={copyIndex > 0}\n            ref={copyIndex === 0 ? seqRef : undefined}\n          >\n            {logos.map((item, itemIndex) =>\n              renderLogoItem(item, `${copyIndex}-${itemIndex}`)\n            )}\n          </ul>\n        )),\n      [copyCount, logos, renderLogoItem, isVertical]\n    )\n\n    const containerStyle = useMemo(\n      (): React.CSSProperties => ({\n        width: isVertical\n          ? toCssLength(width) === \"100%\"\n            ? undefined\n            : toCssLength(width)\n          : (toCssLength(width) ?? \"100%\"),\n        ...cssVariables,\n        ...style,\n      }),\n      [width, cssVariables, style, isVertical]\n    )\n\n    return (\n      <div\n        ref={containerRef}\n        className={rootClasses}\n        style={containerStyle}\n        role=\"region\"\n        aria-label={ariaLabel}\n      >\n        {fadeOut && (\n          <>\n            {isVertical ? (\n              <>\n                <div\n                  aria-hidden\n                  className={cx(\n                    \"pointer-events-none absolute inset-x-0 top-0 z-10\",\n                    \"h-[clamp(24px,8%,120px)]\",\n                    \"bg-[linear-gradient(to_bottom,var(--logoloop-fadeColor,var(--logoloop-fadeColorAuto))_0%,rgba(0,0,0,0)_100%)]\"\n                  )}\n                />\n                <div\n                  aria-hidden\n                  className={cx(\n                    \"pointer-events-none absolute inset-x-0 bottom-0 z-10\",\n                    \"h-[clamp(24px,8%,120px)]\",\n                    \"bg-[linear-gradient(to_top,var(--logoloop-fadeColor,var(--logoloop-fadeColorAuto))_0%,rgba(0,0,0,0)_100%)]\"\n                  )}\n                />\n              </>\n            ) : (\n              <>\n                <div\n                  aria-hidden\n                  className={cx(\n                    \"pointer-events-none absolute inset-y-0 left-0 z-10\",\n                    \"w-[clamp(24px,8%,120px)]\",\n                    \"bg-[linear-gradient(to_right,var(--logoloop-fadeColor,var(--logoloop-fadeColorAuto))_0%,rgba(0,0,0,0)_100%)]\"\n                  )}\n                />\n                <div\n                  aria-hidden\n                  className={cx(\n                    \"pointer-events-none absolute inset-y-0 right-0 z-10\",\n                    \"w-[clamp(24px,8%,120px)]\",\n                    \"bg-[linear-gradient(to_left,var(--logoloop-fadeColor,var(--logoloop-fadeColorAuto))_0%,rgba(0,0,0,0)_100%)]\"\n                  )}\n                />\n              </>\n            )}\n          </>\n        )}\n\n        <div\n          className={cx(\n            \"relative z-0 flex will-change-transform select-none\",\n            \"motion-reduce:transform-none\",\n            isVertical ? \"h-max w-full flex-col\" : \"w-max flex-row\"\n          )}\n          ref={trackRef}\n          onMouseEnter={handleMouseEnter}\n          onMouseLeave={handleMouseLeave}\n        >\n          {logoLists}\n        </div>\n      </div>\n    )\n  }\n)\n\nLogoLoop.displayName = \"LogoLoop\"\n\nexport default LogoLoop\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}