{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "LogoLoop-TS-CSS",
	"title": "LogoLoop",
	"description": "Continuously looping marquee of brand or tech logos with seamless repeat and hover pause.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:file",
			"path": "LogoLoop.css",
			"target": "@components/LogoLoop.css",
			"content": ".logoloop {\n  position: relative;\n  overflow-x: hidden;\n\n  --logoloop-gap: 32px;\n  --logoloop-logoHeight: 28px;\n  --logoloop-fadeColorAuto: #ffffff;\n}\n\n.logoloop--vertical {\n  overflow: hidden;\n  height: 100%;\n  display: inline-block;\n}\n\n.logoloop--scale-hover {\n  padding-top: calc(var(--logoloop-logoHeight) * 0.1);\n  padding-bottom: calc(var(--logoloop-logoHeight) * 0.1);\n}\n\n@media (prefers-color-scheme: dark) {\n  .logoloop {\n    --logoloop-fadeColorAuto: #0b0b0b;\n  }\n}\n\n.logoloop__track {\n  display: flex;\n  width: max-content;\n  will-change: transform;\n  user-select: none;\n  position: relative;\n  z-index: 0;\n}\n\n.logoloop--vertical .logoloop__track {\n  flex-direction: column;\n  height: max-content;\n  width: 100%;\n}\n\n.logoloop__list {\n  display: flex;\n  align-items: center;\n}\n\n.logoloop--vertical .logoloop__list {\n  flex-direction: column;\n}\n\n.logoloop__item {\n  flex: 0 0 auto;\n  margin-right: var(--logoloop-gap);\n  font-size: var(--logoloop-logoHeight);\n  line-height: 1;\n}\n\n.logoloop--vertical .logoloop__item {\n  margin-right: 0;\n  margin-bottom: var(--logoloop-gap);\n}\n\n.logoloop__item:last-child {\n  margin-right: var(--logoloop-gap);\n}\n\n.logoloop--vertical .logoloop__item:last-child {\n  margin-right: 0;\n  margin-bottom: var(--logoloop-gap);\n}\n\n.logoloop__node {\n  display: inline-flex;\n  align-items: center;\n}\n\n.logoloop__item img {\n  height: var(--logoloop-logoHeight);\n  width: auto;\n  display: block;\n  object-fit: contain;\n  image-rendering: -webkit-optimize-contrast;\n  -webkit-user-drag: none;\n  pointer-events: none;\n  transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);\n}\n\n.logoloop--scale-hover .logoloop__item {\n  overflow: visible;\n}\n\n.logoloop--scale-hover .logoloop__item:hover img,\n.logoloop--scale-hover .logoloop__item:hover .logoloop__node {\n  transform: scale(1.2);\n  transform-origin: center center;\n}\n\n.logoloop--scale-hover .logoloop__node {\n  transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);\n}\n\n.logoloop__link {\n  display: inline-flex;\n  align-items: center;\n  text-decoration: none;\n  border-radius: 4px;\n  transition: opacity 0.2s ease;\n}\n\n.logoloop__link:hover {\n  opacity: 0.8;\n}\n\n.logoloop__link:focus-visible {\n  outline: 2px solid currentColor;\n  outline-offset: 2px;\n}\n\n.logoloop--fade::before,\n.logoloop--fade::after {\n  content: '';\n  position: absolute;\n  top: 0;\n  bottom: 0;\n  width: clamp(24px, 8%, 120px);\n  pointer-events: none;\n  z-index: 10;\n}\n\n.logoloop--fade::before {\n  left: 0;\n  background: linear-gradient(\n    to right,\n    var(--logoloop-fadeColor, var(--logoloop-fadeColorAuto)) 0%,\n    rgba(0, 0, 0, 0) 100%\n  );\n}\n\n.logoloop--fade::after {\n  right: 0;\n  background: linear-gradient(\n    to left,\n    var(--logoloop-fadeColor, var(--logoloop-fadeColorAuto)) 0%,\n    rgba(0, 0, 0, 0) 100%\n  );\n}\n\n.logoloop--vertical.logoloop--fade::before,\n.logoloop--vertical.logoloop--fade::after {\n  left: 0;\n  right: 0;\n  width: 100%;\n  height: clamp(24px, 8%, 120px);\n}\n\n.logoloop--vertical.logoloop--fade::before {\n  top: 0;\n  bottom: auto;\n  background: linear-gradient(\n    to bottom,\n    var(--logoloop-fadeColor, var(--logoloop-fadeColorAuto)) 0%,\n    rgba(0, 0, 0, 0) 100%\n  );\n}\n\n.logoloop--vertical.logoloop--fade::after {\n  bottom: 0;\n  top: auto;\n  background: linear-gradient(\n    to top,\n    var(--logoloop-fadeColor, var(--logoloop-fadeColorAuto)) 0%,\n    rgba(0, 0, 0, 0) 100%\n  );\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .logoloop__track {\n    transform: translate3d(0, 0, 0) !important;\n  }\n\n  .logoloop__item img,\n  .logoloop__node {\n    transition: none !important;\n  }\n}\n"
		},
		{
			"type": "registry:component",
			"path": "LogoLoop.tsx",
			"content": "import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport './LogoLoop.css';\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 useResizeObserver = (\n  callback: () => void,\n  elements: Array<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 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    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 = isHovered && hoverSpeed !== undefined ? hoverSpeed : targetVelocity;\n\n      const easingFactor = 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>(ANIMATION_CONFIG.MIN_COPIES);\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 = 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 = containerRef.current?.clientHeight ?? parentHeight ?? sequenceHeight;\n          const copiesNeeded = Math.ceil(viewport / sequenceHeight) + 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 = Math.ceil(containerWidth / sequenceWidth) + ANIMATION_CONFIG.COPY_HEADROOM;\n        setCopyCount(Math.max(ANIMATION_CONFIG.MIN_COPIES, copiesNeeded));\n      }\n    }, [isVertical]);\n\n    useResizeObserver(updateDimensions, [containerRef, seqRef], [logos, gap, logoHeight, isVertical]);\n\n    useImageLoader(seqRef, updateDimensions, [logos, gap, logoHeight, isVertical]);\n\n    useAnimationLoop(trackRef, targetVelocity, seqWidth, seqHeight, isHovered, effectiveHoverSpeed, isVertical);\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 rootClassName = useMemo(\n      () =>\n        [\n          'logoloop',\n          isVertical ? 'logoloop--vertical' : 'logoloop--horizontal',\n          fadeOut && 'logoloop--fade',\n          scaleOnHover && 'logoloop--scale-hover',\n          className\n        ]\n          .filter(Boolean)\n          .join(' '),\n      [isVertical, fadeOut, 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 className=\"logoloop__item\" key={key} role=\"listitem\">\n              {renderItem(item, key)}\n            </li>\n          );\n        }\n        const isNodeItem = 'node' in item;\n        const content = isNodeItem ? (\n          <span className=\"logoloop__node\" aria-hidden={!!item.href && !item.ariaLabel}>\n            {(item as any).node}\n          </span>\n        ) : (\n          <img\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        const itemAriaLabel = isNodeItem\n          ? ((item as any).ariaLabel ?? (item as any).title)\n          : ((item as any).alt ?? (item as any).title);\n        const itemContent = (item as any).href ? (\n          <a\n            className=\"logoloop__link\"\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        return (\n          <li className=\"logoloop__item\" key={key} role=\"listitem\">\n            {itemContent}\n          </li>\n        );\n      },\n      [renderItem]\n    );\n\n    const logoLists = useMemo(\n      () =>\n        Array.from({ length: copyCount }, (_, copyIndex) => (\n          <ul\n            className=\"logoloop__list\"\n            key={`copy-${copyIndex}`}\n            role=\"list\"\n            aria-hidden={copyIndex > 0}\n            ref={copyIndex === 0 ? seqRef : undefined}\n          >\n            {logos.map((item, itemIndex) => renderLogoItem(item, `${copyIndex}-${itemIndex}`))}\n          </ul>\n        )),\n      [copyCount, logos, renderLogoItem]\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 ref={containerRef} className={rootClassName} style={containerStyle} role=\"region\" aria-label={ariaLabel}>\n        <div className=\"logoloop__track\" ref={trackRef} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>\n          {logoLists}\n        </div>\n      </div>\n    );\n  }\n);\n\nLogoLoop.displayName = 'LogoLoop';\n\nexport default LogoLoop;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": []
}