{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "marquee",
  "type": "registry:ui",
  "title": "Marquee",
  "description": "An accessible React marquee for Next.js and Tailwind CSS: an endless, seamless row of logos or cards that pauses on hover and stops with reduced motion.",
  "author": "FewTone <https://fewtone.com>",
  "dependencies": [
    "motion@^14.0.0"
  ],
  "registryDependencies": [
    "https://ui.fewtone.com/r/tokens.json",
    "https://ui.fewtone.com/r/motion.json"
  ],
  "files": [
    {
      "path": "registry/fewtone/ui/marquee.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport { duration, ease } from \"@/lib/motion\";\nimport { cn } from \"@/lib/utils\";\nimport { animate, useReducedMotion } from \"motion/react\";\nimport {\n  type ComponentProps,\n  type CSSProperties,\n  type FocusEvent,\n  type PointerEvent,\n  useCallback,\n  useEffect,\n  useLayoutEffect,\n  useRef,\n  useState,\n  useSyncExternalStore,\n} from \"react\";\n\n/*\n * Marquee: a row of logos or cards that scrolls on its own (owner's roadmap, 2026-10-16).\n * - Seamless: the children are repeated until they cover the row with one copy to spare, and the track slides\n *   exactly one copy (and a gap) before starting over, which looks the same, at `speed` pixels a second. Only a\n *   transform moves, on the Web Animations API, so the compositor runs it without React or the main thread.\n * - It rests where nobody's looking: paused off-screen (IntersectionObserver) and while the tab is hidden.\n * - The crafted part: hover (pauseOnHover) and keyboard focus inside slow it to a stop over duration.slow rather\n *   than freezing it, and it picks up speed again when you leave, from exactly where it stopped. `paused` stops\n *   it the same way, for a pause control (WCAG 2.2.2: moving content that lasts needs a way to stop it).\n *   Only at full speed does the loop run by itself. While it slows or picks up it's paused and stepped by hand,\n *   frame by frame: changing playbackRate every frame on an animation the compositor runs let the two clocks\n *   drift, so leaving the hover snapped it to where it would have been without one (owner, 2026-10-21).\n * - The copies are aria-hidden and inert, so screen readers and Tab meet each item once.\n * - The edges fade through a static mask, so items arrive and leave softly on any surface.\n * - With reduced motion it doesn't move: one copy, in a row you can scroll.\n */\n\n/** Each end fades over a tenth of the row */\nconst fades = {\n  horizontal: \"linear-gradient(to right, transparent, black 10%, black 90%, transparent)\",\n  vertical: \"linear-gradient(to bottom, transparent, black 10%, black 90%, transparent)\",\n} as const;\n\nconst subscribe = () => () => {};\n\n/** The loop runs by itself only at full speed (`free`), and only while it can be seen: on screen, in a visible tab */\nfunction syncRunning(loop: Animation | null, onScreen: boolean, free: boolean) {\n  if (!loop) return;\n  const run = free && onScreen && document.visibilityState !== \"hidden\";\n  if (run && loop.playState === \"paused\") loop.play();\n  else if (!run && loop.playState === \"running\") loop.pause();\n}\n\nexport type MarqueeProps = ComponentProps<\"div\"> & {\n  /** Which way the items travel. Up and down need a height on the marquee (`className=\"h-72\"`) */\n  direction?: \"left\" | \"right\" | \"up\" | \"down\";\n  /** Pixels a second */\n  speed?: number;\n  /** Slow to a stop while the pointer is over it */\n  pauseOnHover?: boolean;\n  /** Stopped (it slows to a stop, and picks up again when set back) */\n  paused?: boolean;\n  /** Space between items, and between the copies: a CSS length, or pixels. gap-4 (1rem) if left out */\n  gap?: number | string;\n  /** Fade the items out at the ends */\n  fade?: boolean;\n};\n\n/**\n * A row of items that scrolls on its own, round and round. Put the items straight inside: logos, cards, quotes.\n */\nfunction Marquee({\n  direction = \"left\",\n  speed = 40,\n  pauseOnHover = true,\n  paused = false,\n  gap,\n  fade = true,\n  className,\n  style,\n  children,\n  ref,\n  onPointerEnter,\n  onPointerLeave,\n  onFocus,\n  onBlur,\n  ...props\n}: MarqueeProps) {\n  const vertical = direction === \"up\" || direction === \"down\";\n  const reduce = useReducedMotion() ?? false;\n  // Read only after hydration, so the server's HTML (always the moving row) is what hydrates\n  const hydrated = useSyncExternalStore(\n    subscribe,\n    () => true,\n    () => false,\n  );\n  const still = hydrated && reduce;\n\n  const root = useRef<HTMLDivElement | null>(null);\n  const track = useRef<HTMLDivElement>(null);\n  const loop = useRef<Animation | null>(null);\n  // How far through a loop it was, kept when the loop is rebuilt (a resize, a new speed)\n  const phase = useRef(0);\n  const onScreen = useRef(true);\n  const [copies, setCopies] = useState(2);\n  /** One copy and one gap, in pixels: the distance of a loop */\n  const [length, setLength] = useState(0);\n  const [hovered, setHovered] = useState(false);\n  const [focused, setFocused] = useState(false);\n  const held = paused || focused || (pauseOnHover && hovered);\n  // The loop's speed, 0 to 1. It starts where the props say, so nothing glides on first paint.\n  const rate = useRef(held ? 0 : 1);\n\n  const setRef = useCallback(\n    (node: HTMLDivElement | null) => {\n      root.current = node;\n      if (typeof ref === \"function\") ref(node);\n      else if (ref) ref.current = node;\n    },\n    [ref],\n  );\n\n  // Measure a loop and how many copies cover the row, again whenever either resizes\n  useLayoutEffect(() => {\n    const node = root.current;\n    const row = track.current;\n    if (!node || !row || still) return;\n    const measure = () => {\n      const [first, second] = Array.from(row.children) as HTMLElement[];\n      if (!first || !second) return;\n      // Right to left, the copies run leftwards, so the distance is the same either way\n      const next = Math.abs(\n        vertical ? second.offsetTop - first.offsetTop : second.offsetLeft - first.offsetLeft,\n      );\n      const room = vertical ? node.clientHeight : node.clientWidth;\n      setLength(next);\n      setCopies(next > 0 ? Math.max(2, Math.ceil(room / next) + 1) : 2);\n    };\n    measure();\n    const observer = new ResizeObserver(measure);\n    observer.observe(node);\n    if (row.firstElementChild) observer.observe(row.firstElementChild);\n    return () => observer.disconnect();\n  }, [vertical, still]);\n\n  // The loop: one copy's length, linear, forever\n  useEffect(() => {\n    const row = track.current;\n    if (!row || still || length <= 0 || speed <= 0 || typeof row.animate !== \"function\") return;\n    const axis = vertical ? \"translateY\" : \"translateX\";\n    // Right to left, the copies start at the right edge and run leftwards, so the loop sits one copy further\n    // right: the first copy always comes back to its own place\n    const shift = !vertical && getComputedStyle(row).direction === \"rtl\" ? length : 0;\n    const ends = (direction === \"left\" || direction === \"up\" ? [0, -length] : [-length, 0]).map(\n      (end) => end + shift,\n    );\n    const ms = (length / speed) * 1000;\n    const animation = row.animate(\n      ends.map((end) => ({ transform: `${axis}(${end}px)` })),\n      { duration: ms, iterations: Number.POSITIVE_INFINITY, easing: \"linear\" },\n    );\n    animation.currentTime = phase.current * ms;\n    loop.current = animation;\n    syncRunning(animation, onScreen.current, rate.current === 1);\n    return () => {\n      phase.current = ((Number(animation.currentTime) || 0) / ms) % 1;\n      animation.cancel();\n      loop.current = null;\n    };\n  }, [length, speed, direction, vertical, still]);\n\n  // Rest off-screen and in a hidden tab\n  useEffect(() => {\n    const node = root.current;\n    if (!node || still) return;\n    const update = () => syncRunning(loop.current, onScreen.current, rate.current === 1);\n    let observer: IntersectionObserver | undefined;\n    if (typeof IntersectionObserver !== \"undefined\") {\n      observer = new IntersectionObserver((entries) => {\n        const entry = entries.at(-1);\n        onScreen.current = entry?.isIntersecting ?? true;\n        update();\n      });\n      observer.observe(node);\n    }\n    document.addEventListener(\"visibilitychange\", update);\n    return () => {\n      observer?.disconnect();\n      document.removeEventListener(\"visibilitychange\", update);\n    };\n  }, [still]);\n\n  // Hover, focus and `paused` glide the speed to 0 and back, rather than freezing it. Meanwhile the loop is\n  // paused and its time stepped by hand, at the gliding speed, so it never leaves the place it's drawn at.\n  useEffect(() => {\n    const target = held ? 0 : 1;\n    if (rate.current === target) return;\n    let last = performance.now();\n    // Below full speed from here on: the loop stops running by itself\n    if (rate.current === 1) rate.current = 1 - Number.EPSILON;\n    syncRunning(loop.current, onScreen.current, false);\n    const glide = animate(rate.current, target, {\n      duration: reduce ? 0 : duration.slow,\n      ease: ease.out,\n      onUpdate: (value) => {\n        const now = performance.now();\n        const animation = loop.current;\n        if (animation && onScreen.current && document.visibilityState !== \"hidden\")\n          animation.currentTime = (Number(animation.currentTime) || 0) + (now - last) * value;\n        last = now;\n        rate.current = value;\n      },\n      onComplete: () => {\n        rate.current = target;\n        // Back to full speed: it runs by itself again, on from where it is\n        syncRunning(loop.current, onScreen.current, rate.current === 1);\n      },\n    });\n    return () => glide.stop();\n  }, [held, reduce]);\n\n  const spacing: CSSProperties | undefined =\n    gap === undefined ? undefined : { gap: typeof gap === \"number\" ? `${gap}px` : gap };\n  const groups = Array.from({ length: still ? 1 : copies }, (_, copy) => copy);\n  const mask = fade && !still ? fades[vertical ? \"vertical\" : \"horizontal\"] : undefined;\n\n  return (\n    // biome-ignore lint/a11y/noStaticElementInteractions: hover and focus only slow the loop down; nothing here is pressed\n    <div\n      ref={setRef}\n      data-slot=\"marquee\"\n      data-direction={direction}\n      data-orientation={vertical ? \"vertical\" : \"horizontal\"}\n      data-paused={held || undefined}\n      // With reduced motion it's a row to scroll, so the keyboard can reach it too\n      tabIndex={still ? 0 : undefined}\n      className={cn(\n        \"relative flex overflow-hidden\",\n        vertical && \"flex-col\",\n        still && (vertical ? \"overflow-y-auto\" : \"overflow-x-auto\"),\n        \"outline-ring focus-visible:outline-(length:--focus-ring-width) focus-visible:outline-offset-(--focus-ring-offset)\",\n        className,\n      )}\n      style={mask ? { maskImage: mask, WebkitMaskImage: mask, ...style } : style}\n      onPointerEnter={(event: PointerEvent<HTMLDivElement>) => {\n        onPointerEnter?.(event);\n        if (event.pointerType === \"mouse\") setHovered(true);\n      }}\n      onPointerLeave={(event: PointerEvent<HTMLDivElement>) => {\n        onPointerLeave?.(event);\n        setHovered(false);\n      }}\n      onFocus={(event: FocusEvent<HTMLDivElement>) => {\n        onFocus?.(event);\n        setFocused(true);\n      }}\n      onBlur={(event: FocusEvent<HTMLDivElement>) => {\n        onBlur?.(event);\n        if (!event.currentTarget.contains(event.relatedTarget as Node | null)) setFocused(false);\n      }}\n      {...props}\n    >\n      <div\n        ref={track}\n        data-slot=\"marquee-track\"\n        className={cn(\"flex shrink-0 gap-4\", vertical ? \"h-max flex-col\" : \"w-max\")}\n        style={spacing}\n      >\n        {groups.map((copy) => (\n          // The first copy is the content; the others are only there to fill the row\n          <div\n            key={copy}\n            data-slot=\"marquee-group\"\n            aria-hidden={copy > 0 || undefined}\n            inert={copy > 0 || undefined}\n            className={cn(\"flex shrink-0 gap-4\", vertical && \"flex-col\")}\n            style={spacing}\n          >\n            {children}\n          </div>\n        ))}\n      </div>\n    </div>\n  );\n}\n\nexport { Marquee };\n"
    }
  ],
  "categories": [
    "effects"
  ],
  "docs": "Usage and examples: https://ui.fewtone.com/components/marquee"
}
