{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "marquee",
  "title": "Marquee",
  "description": "A row of content that runs past without a seam, for the places where a list is longer than the space given to it and the point is the list rather than any one entry. Reach for it for a wall of customer or partner logos, a strip of press mentions, \"as seen in\" or integration badges, the speaker line-up on a conference page, a status or incident band along the top of a dashboard, a notice scrolling across a lobby or kiosk display, a ticker of recent signups, reviews or trades, a tag cloud that will not fit, or a screenshot reel on a landing page. Common asks it answers: \"marquee react\", \"infinite scrolling logos react\", \"seamless infinite scroll carousel\", \"logo cloud auto scroll\", \"css marquee without jank\", \"ticker tape component react\", \"news ticker react\", \"auto scrolling text banner\", \"continuous horizontal scroll animation\", \"shadcn marquee\", \"logo marquee tailwind\", \"scrolling partner logos\", \"infinite loop animation react\", \"magicui marquee\", \"react-fast-marquee\", \"react-marquee-slider\", \"marquee pause on hover\", \"marquee accessibility\", \"prefers-reduced-motion marquee\", \"how many copies for a seamless marquee\", \"marquee jumps at the loop\", \"marquee speed depends on how many items\", \"my marquee duplicates screen reader output\", \"duplicate links in marquee tab order\", \"WCAG pause stop hide marquee\", \"横スクロールする帯 react\", \"ロゴを無限に流す\", \"繋ぎ目のないマーキー\", \"ニュースティッカー 実装\", \"ホバーで止まる流れる帯\", \"流れる帯 アクセシビリティ\". **The seam is the whole problem, and the only way to hide it is to duplicate the content.** One copy sliding left eventually drags its own end into view, so the content is rendered several times over and the track travels exactly the distance from one copy to the next — at which point every copy stands where the one before it stood and the jump back to the start has nothing to give it away. Measured on a Web Animations loop, the computed transform at the end of an iteration is identical to the transform at the start. **The duplicates are `aria-hidden` and `inert`, and both are needed.** Measured in a browser, `aria-hidden` on its own leaves a clone's links and buttons focusable, so a wall of three logo links rendered in four copies hands a keyboard user twelve tab stops, nine of which lead somewhere a screen reader has been told is not there. `inert` takes them out of the tab order as well, and keeps a clone from swallowing a click meant for the real thing. Only the first copy is in the accessibility tree, so the list is read once. **Speed is pixels per second, not a duration**, because a duration is not a speed: three logos and thirty logos both set to \"20s\" travel at a tenth of each other's pace, so every addition to the list silently retunes the animation. Here the duration is computed from the distance that was measured, and adding a logo does not change how fast the band reads. **How many copies it takes is arithmetic, and the term everybody drops is the gap.** A loop moves the track by one step, so the copies have to reach `step + box width`; `n` copies reach `n * step - gap`, not `n * step`, because the gap trails every copy but the last. That makes the count `ceil((box + gap) / step) + 1`. The version without the gap is right almost everywhere — swept over 27 combinations of gap and box width it was short in one, and in that one (a 40px gap, a 300px box, a 316.44px step) it left 23.56px of emptiness at the trailing edge at the moment of fullest travel, once per loop, forever. The gap is never asked for: it is the measured step minus the measured copy width, so a gap set from anywhere still agrees with the layout. **The distance is read as a difference between two rectangles, never from `offsetLeft` or `offsetWidth`.** Those are rounded integers, and taking the step from them came out 0.5px and 0.4375px wrong in the two layouts measured — half a pixel of jump at the seam on every loop, for nothing. A difference also includes the gap without asking about it, and is immune to the transform the track is already under: the same two copies measured 268.4375 apart both at rest and mid-animation, while the absolute left of one of them had moved from 0 to −109.56. **The track is out of flow, with one hidden copy left in flow to size the box, and that is not a stylistic choice.** The track is as wide as the copy count, so a box taking its width from its content gets wider every time the count goes up, and a wider box needs a higher count: measured inside an `inline-flex` parent that ran 210px → 444px → 912px → 1848px as the count doubled, with nothing to stop it. With the track absolutely positioned and one copy's worth of content in flow behind it, the width came out identical at 1, 2, 4, 8 and 16 copies in all four parent layouts tried. The same hidden copy gives the box its height, so the band is the right height in the server's HTML rather than after a measurement — and it is intrinsically sized, because a spacer narrow enough to squeeze its items wraps them and reported 60px of height where the copies were 24px. **The box clips without becoming something that can be scrolled.** `overflow: hidden` makes a scroll container, and a browser scrolls one to reveal a focused descendant — measured, tabbing to the last link of the visible copy in a box narrower than that copy scrolled the box to 284 and left it there, sliding the whole band sideways for good while the animation carried on from the wrong place. `overflow: clip` clips identically without the scroll container: the same focus left it at 0 and did not scroll the page either. The class stays underneath as the fallback for an engine that drops the declaration. **Under `prefers-reduced-motion: reduce` this is a different component, not a stopped one.** A stopped marquee is a list with its end cut off: measured on a 300px box, 233px of the content sat outside it. Reduce gets a plain wrapping list instead, which measured 0px of overflow with every item inside the box. The fallback has to be a different tree rather than a flag because of the layout — the copies must refuse to shrink for the track to be wider than the box, and refusing to shrink is exactly what stops them wrapping. The media query is subscribed to, so turning the setting on mid-visit stops the band without a reload. **Hover and focus stop it, and the button is the part that matters.** Moving content that runs for more than five seconds needs a way to stop it (WCAG 2.2.2), and hover is not a way for anyone without a mouse, so the pause button is on by default, reachable by keyboard whether or not it is visible, and visible from the start on a device with no hover to reveal it with. Hover is ignored for touch pointers on purpose: a tap fires pointerenter with no pointer left to leave, and a band stopped for the rest of a visit because a thumb brushed a logo is worse than one that never stopped. Pausing holds the content where it is and resuming carries on from there rather than from the top — measured resuming at 806ms from a pause taken at 700ms — and anything that changes the distance (a resize, a web font arriving, a logo added) rebuilds the animation at the phase the old one had reached, because a rebuilt animation otherwise starts at zero and snaps the band back: measured, that put the track at translateX(0) where it had been at −134.219, while carrying the phase across reproduced −134.219 exactly. It animates through the Web Animations API rather than CSS keyframes, because the distance is a measurement and keyframes carrying a measured distance would have to be generated per instance — which a registry component cannot do to someone else's stylesheet. The animation is declarative either way, so this is not a `requestAnimationFrame` loop. `data-state` is \"running\" or \"paused\" for styling the stopped state, `paused` and `onPausedChange` hand that state to the caller, `direction` runs it rightwards, and `marqueeCopyCount` is exported so the arithmetic can be reused or checked. Content is cloned, so a child that holds state or costs something to render is rendered several times over; logos, names and short text are the intended cargo. Theme tokens only, no hardcoded colours, no dependencies beyond React and lucide-react for the two icons. **Official shadcn/ui has nothing here, and the measurement is not close.** Fetching every entry in its registry today — 65 listed, 62 fetchable, code-block and questionnaire and trace 404 on both style tracks — and searching all 223,287 bytes of it, the strings marquee, ticker, scrollAmount, prefers-reduced-motion, keyframes, animation and playbackRate are every one of them a zero hit. Official transitions between states 47 times and animates nothing at all: its single `translateX` is the progress bar holding its indicator at a fixed offset. Within pulld the nearest neighbours are tab-scroller and scroll-shadow, which both react to scrolling rather than moving by themselves, and announcement-bar, which is a band that holds still; sortable-list and swipe-actions move under a pointer rather than on their own. Nothing else in the registry runs continuously, and nothing else duplicates its children.",
  "dependencies": [
    "lucide-react"
  ],
  "files": [
    {
      "path": "registry/ui/marquee.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Pause, Play } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst REDUCED_MOTION = \"(prefers-reduced-motion: reduce)\"\n\n/** Pixels of travel per second when the caller has not said. */\nconst DEFAULT_SPEED = 48\n\n/** Space between items, in px, when the caller has not said. */\nconst DEFAULT_GAP = 24\n\n/**\n * The fewest copies ever rendered.\n *\n * Two, because the second copy is the ruler: the distance one loop travels is measured as the\n * distance from the start of the first copy to the start of the second, and a track holding one\n * copy has nothing to measure against. It is also the floor of what a seamless loop needs —\n * something has to be arriving behind the content that is leaving.\n */\nconst MIN_COPIES = 2\n\nconst useIsomorphicLayoutEffect =\n  typeof window === \"undefined\" ? React.useEffect : React.useLayoutEffect\n\n/**\n * `Animation.currentTime` in milliseconds.\n *\n * The DOM type is `CSSNumberish`, which is a plain number for every animation created the way this\n * one is — a numeric duration on a document timeline — and a `CSSNumericValue` only on a timeline\n * measuring something other than time. Falling back to zero rather than unwrapping a unit value\n * keeps the type honest, and the cost of being wrong is that a rebuilt animation starts at the top\n * of its loop, which is what it would have done without any of this.\n */\nconst millis = (time: Animation[\"currentTime\"]): number =>\n  typeof time === \"number\" ? time : 0\n\n/**\n * Whether this visitor has asked for less motion, read from the media query rather than from state.\n *\n * Used to decide whether to start an animation, where the answer is needed before any state has\n * settled. Which tree to render is a separate question answered from state — see `reduced` below.\n */\nfunction prefersReducedMotion(): boolean {\n  if (typeof window === \"undefined\" || typeof window.matchMedia !== \"function\") return false\n  return window.matchMedia(REDUCED_MOTION).matches\n}\n\nexport interface MarqueeCopyCountOptions {\n  /** Distance from the start of one copy to the start of the next, gap included, in px. */\n  step: number\n  /** Width of a single copy, in px, not counting the gap that follows it. */\n  copyWidth: number\n  /** Width of the visible box the copies travel through, in px. */\n  viewportWidth: number\n}\n\n/**\n * How many copies of the content it takes to keep the box full for a whole loop.\n *\n * One loop moves the track left by exactly `step`, at which point every copy stands where the one\n * before it stood and the jump back to the start is invisible. For the box to stay full throughout,\n * the copies have to reach `step + viewportWidth`: by the end of the travel the first `step` pixels\n * have left the box and what remains has to cover all of it.\n *\n * `n` copies reach `n * step - gap`, not `n * step`, because the gap trails every copy but the\n * last. So the requirement is `n * step - gap >= step + viewportWidth`, which is\n * `n = ceil((viewportWidth + gap) / step) + 1`.\n *\n * **The gap is the term that gets dropped, and dropping it shows.** `ceil(viewportWidth / step) + 1`\n * is what this was first written as, and it is right almost everywhere: swept over 27 combinations\n * of gap and box width it was short in one. In that one — a 40px gap, a 300px box, a 316.44px step\n * — it left **23.56px of emptiness** at the trailing edge at the moment of fullest travel, once per\n * loop. The exact form was short in none of the 27.\n *\n * The gap is not a parameter because it need not be: it is `step - copyWidth`, and both of those\n * are measured off the rendered track. A gap taken from a prop or read back from CSS would disagree\n * with the layout the moment anything else set one.\n */\nexport function marqueeCopyCount({\n  step,\n  copyWidth,\n  viewportWidth,\n}: MarqueeCopyCountOptions): number {\n  // Nothing has been laid out, or there is nothing to lay out. A box inside a closed dialog or an\n  // unselected tab measures zero on every axis, exactly as empty content does, and neither is a\n  // division worth doing: `ceil(width / 0)` is Infinity, and a copy count of Infinity is a render\n  // that never returns.\n  if (!(step > 0) || !(copyWidth > 0) || !Number.isFinite(viewportWidth)) return MIN_COPIES\n  const gap = Math.max(step - copyWidth, 0)\n  // A negative box width needs no clamp of its own: the arithmetic returns something below two and\n  // the floor takes it from there.\n  return Math.max(Math.ceil((viewportWidth + gap) / step) + 1, MIN_COPIES)\n}\n\n/** What was measured off the track, and what it implies. */\ninterface Geometry {\n  /** Start-to-start distance between copies, gap included, in px. 0 until measured. */\n  step: number\n  /** Width of one copy, in px. 0 until measured, and 0 for a box with no layout. */\n  copyWidth: number\n  /** Copies to render. */\n  copies: number\n}\n\nconst UNMEASURED: Geometry = { step: 0, copyWidth: 0, copies: MIN_COPIES }\n\nexport interface MarqueeProps extends React.HTMLAttributes<HTMLDivElement> {\n  /** The items to run past. Cloned, so keep them presentational — see the note on the component. */\n  children: React.ReactNode\n  /** Travel in pixels per second. Defaults to 48. */\n  speed?: number\n  /** Which way the content travels. Defaults to \"left\". */\n  direction?: \"left\" | \"right\"\n  /** Space between items, in px. Defaults to 24. */\n  gap?: number\n  /** Stop while a pointer that can hover is over it. Defaults to true. */\n  pauseOnHover?: boolean\n  /** Stop while focus is inside it. Defaults to true. */\n  pauseOnFocus?: boolean\n  /** Render the pause button. Defaults to true — it is the only way to stop this without a mouse. */\n  controls?: boolean\n  /** Hold the pause from outside. Leave it out to let the button own that state. */\n  paused?: boolean\n  /** Told when the pause is turned on or off. */\n  onPausedChange?: (paused: boolean) => void\n  /** Accessible name of the button while the content is moving. */\n  pauseLabel?: string\n  /** Accessible name of the button while it is held stopped. */\n  resumeLabel?: string\n}\n\n/**\n * A row of content that runs past without a seam.\n *\n * For the places where a list is longer than the space given to it and the point is the list rather\n * than any one entry: a wall of customer logos, a strip of press mentions or integrations, the\n * speaker line-up on a conference page, a status band along the top of a dashboard, a notice\n * scrolling across a lobby display, a ticker of recent signups.\n *\n * **The seam is the whole problem, and the only way to hide it is to duplicate the content.** One\n * copy sliding left eventually drags its own end into view, so the content is rendered several\n * times over and the track travels exactly the distance from one copy to the next — at which point\n * every copy stands where the one before it stood, and the jump back to the start has nothing to\n * give it away. Measured on a Web Animations loop, the computed transform at the end of an\n * iteration is identical to the transform at the start.\n *\n * **The duplicates are `aria-hidden` *and* `inert`, and both are needed.** `aria-hidden` alone\n * leaves a clone's links and buttons focusable — measured — so a wall of three logo links rendered\n * in four copies hands a keyboard user twelve tab stops, nine of which lead somewhere a screen\n * reader has been told is not there. `inert` takes them out of the tab order too, and keeps a clone\n * from swallowing a click meant for the real thing.\n *\n * **Speed is pixels per second, not a duration**, because a duration is not a speed: three logos\n * and thirty logos both set to \"20s\" travel at a tenth of each other's pace, so every addition to\n * the list silently retunes the animation. Here the duration is computed from the distance that was\n * measured, so adding a logo does not change how fast the band reads.\n *\n * **Under `prefers-reduced-motion: reduce` this is a different component, not a stopped one.** A\n * stopped marquee is a list with its end cut off — measured on a 300px box, 233px of the content\n * sits outside it — so reduce gets a plain wrapping list instead, which measured 0px of overflow\n * with every item inside the box. The fallback has to be a different tree rather than a flag\n * because of the layout: the copies must refuse to shrink for the track to be wider than the box,\n * and refusing to shrink is exactly what stops them wrapping.\n *\n * **Hover and focus stop it, but the button is the part that matters.** Moving content that runs\n * for more than five seconds needs a way to stop it (WCAG 2.2.2), and hover is not a way for\n * anyone without a mouse. So `controls` defaults to true, the button is reachable by keyboard\n * whether or not it is visible, and where there is no hover to reveal it with it is visible from\n * the start. Pass `controls={false}` only when something else on the page stops this.\n *\n * It animates through the Web Animations API rather than CSS keyframes because the distance is a\n * measurement, and keyframes carrying a measured distance would have to be generated per instance —\n * which a registry component cannot do to someone else's stylesheet. The animation is declarative\n * either way, so this is not a `requestAnimationFrame` loop and does not depend on a frame callback\n * firing; what either does in a backgrounded tab is not something this was able to measure.\n *\n * Content is cloned, so a child that holds state or costs something to render is rendered several\n * times over. Logos, names and short text are the intended cargo.\n */\nexport const Marquee = React.forwardRef<HTMLDivElement, MarqueeProps>(function Marquee(\n  {\n    children,\n    speed = DEFAULT_SPEED,\n    direction = \"left\",\n    gap = DEFAULT_GAP,\n    pauseOnHover = true,\n    pauseOnFocus = true,\n    controls = true,\n    paused,\n    onPausedChange,\n    pauseLabel = \"Pause\",\n    resumeLabel = \"Resume\",\n    className,\n    style,\n    onPointerEnter,\n    onPointerLeave,\n    onPointerCancel,\n    onFocus,\n    onBlur,\n    ...props\n  },\n  forwardedRef\n) {\n  const viewportRef = React.useRef<HTMLDivElement>(null)\n  const spacerRef = React.useRef<HTMLDivElement>(null)\n  const trackRef = React.useRef<HTMLDivElement>(null)\n  React.useImperativeHandle(forwardedRef, () => viewportRef.current as HTMLDivElement)\n\n  const [geometry, setGeometry] = React.useState<Geometry>(UNMEASURED)\n  const [heldInternally, setHeldInternally] = React.useState(false)\n  const [hovered, setHovered] = React.useState(false)\n  const [focusWithin, setFocusWithin] = React.useState(false)\n\n  const held = paused ?? heldInternally\n\n  /**\n   * Reduced motion as state rather than as a read of the media query during render, because it\n   * selects between two different trees and the server has no media query to select with — reading\n   * it in render would disagree with the HTML being hydrated. A layout effect rather than a passive\n   * one, so the swap to the static list lands before the first paint and the clipped row that the\n   * animated tree is until it starts is never shown to somebody who asked for less motion.\n   */\n  const [reduced, setReduced] = React.useState(false)\n  useIsomorphicLayoutEffect(() => {\n    setReduced(prefersReducedMotion())\n    if (typeof window === \"undefined\" || typeof window.matchMedia !== \"function\") return\n    const query = window.matchMedia(REDUCED_MOTION)\n    if (typeof query.addEventListener !== \"function\") return\n    const onChange = () => setReduced(query.matches)\n    query.addEventListener(\"change\", onChange)\n    return () => query.removeEventListener(\"change\", onChange)\n  }, [])\n\n  const measure = React.useCallback(() => {\n    const viewport = viewportRef.current\n    const track = trackRef.current\n    if (!viewport || !track) return\n    const first = track.children[0]\n    const second = track.children[1]\n    if (!first || !second) return\n\n    const firstRect = first.getBoundingClientRect()\n    // A *difference* between two boxes, never either box's own position. The track is under a\n    // transform that is usually mid-travel, so a copy's absolute left is wherever the animation has\n    // got to — measured at −109.56 where at rest it was 0 — while the distance between two copies\n    // reads the same at rest and mid-animation to four decimals.\n    //\n    // It is also the only reading that includes the gap without asking anyone what the gap is, and\n    // the only one with sub-pixel truth: `offsetLeft` is a rounded integer, and taking the step from\n    // `offsetLeft` differences came out 0.5px and 0.4375px wrong in the two layouts measured. That\n    // error is a jump at the seam, every loop, for nothing.\n    const step = second.getBoundingClientRect().left - firstRect.left\n    const copyWidth = firstRect.width\n    const copies = marqueeCopyCount({ step, copyWidth, viewportWidth: viewport.clientWidth })\n    setGeometry((previous) =>\n      previous.step === step && previous.copyWidth === copyWidth && previous.copies === copies\n        ? previous\n        : { step, copyWidth, copies }\n    )\n  }, [])\n\n  // `children` is a dependency because new content is a new width, and the observers below only\n  // notice that one paint later. Re-measuring on a parent's render costs one forced layout and\n  // settles on the same numbers, so it is idempotent rather than free.\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  useIsomorphicLayoutEffect(measure, [measure, reduced, children])\n\n  React.useEffect(() => {\n    if (reduced || typeof ResizeObserver === \"undefined\") return\n    const viewport = viewportRef.current\n    const spacer = spacerRef.current\n    if (!viewport || !spacer) return\n\n    // The box and one copy's worth of content — between them, everything the copy count depends on.\n    // Deliberately *not* the track: the track's width is a function of the count, so an observer on\n    // it would be an observer on this component's own last answer. The spacer stands in for the\n    // content precisely because it is one copy's worth whatever the count is.\n    const observer = new ResizeObserver(measure)\n    observer.observe(viewport)\n    observer.observe(spacer)\n    return () => observer.disconnect()\n  }, [measure, reduced])\n\n  React.useEffect(() => {\n    if (reduced) return\n    // A face arriving after the first measurement changes the width of every glyph without changing\n    // any box, so no observer reports it — the same reason fit-text and overflow-tooltip both wait\n    // on this separately.\n    let cancelled = false\n    const fonts = typeof document !== \"undefined\" ? document.fonts : undefined\n    fonts?.ready.then(() => {\n      if (!cancelled) measure()\n    })\n    return () => {\n      cancelled = true\n    }\n  }, [measure, reduced])\n\n  const stopped = held || (pauseOnHover && hovered) || (pauseOnFocus && focusWithin)\n\n  /** The animation currently on the track, with what it was built from. */\n  const liveRef = React.useRef<{\n    animation: Animation\n    duration: number\n    signature: string\n  } | null>(null)\n  /** Where in its loop the last animation was, as a fraction, so a rebuilt one can resume there. */\n  const phaseRef = React.useRef<number | null>(null)\n  const stoppedRef = React.useRef(stopped)\n  stoppedRef.current = stopped\n  /** The stopped-ness the animation on the track was last put into. */\n  const appliedRef = React.useRef<boolean | null>(null)\n\n  /** Takes the animation off the track, remembering where in its loop it had got to. */\n  const release = React.useCallback(() => {\n    const live = liveRef.current\n    if (!live) return\n    liveRef.current = null\n    phaseRef.current = (millis(live.animation.currentTime) % live.duration) / live.duration\n    live.animation.cancel()\n  }, [])\n\n  React.useEffect(() => {\n    const track = trackRef.current\n    const { step, copyWidth } = geometry\n    // `reduced` is state, and state set in a layout effect does not reach the passive effects of the\n    // commit that set it — this one would run once with `reduced` still false and animate for a\n    // frame before the next commit cancelled it. The media query has no such lag, so it is asked\n    // again here, where the answer decides whether anything moves at all.\n    if (!track || reduced || prefersReducedMotion()) return\n    if (!(step > 0) || !(copyWidth > 0) || !(speed > 0)) return\n    if (typeof track.animate !== \"function\") return\n\n    // An animation is a function of these three numbers and nothing else, so an effect that runs\n    // again with all three unchanged must leave the one already on the track alone: rebuilding it is\n    // a restart, and a restart is visible. Stated here rather than left to the dependency array\n    // because the array says when this *may* run, not what it is allowed to do when it does.\n    const signature = `${direction} ${step} ${speed}`\n    if (liveRef.current?.signature === signature) return\n    release()\n\n    const duration = (step / speed) * 1000\n    const from = direction === \"left\" ? 0 : -step\n    const to = direction === \"left\" ? -step : 0\n    const animation = track.animate(\n      [{ transform: `translateX(${from}px)` }, { transform: `translateX(${to}px)` }],\n      { duration, iterations: Infinity, easing: \"linear\" }\n    )\n    // Resumed at the phase the previous animation reached, because a rebuilt animation starts at\n    // zero and that is a jump: measured, a fresh animation put the track back at translateX(0) from\n    // −134.219, while carrying the fraction across reproduced −134.219 exactly. Everything that\n    // changes the distance rebuilds the animation — a resize, a web font, an added logo — and none\n    // of those is a moment to snap the band back to the top of its loop.\n    if (phaseRef.current !== null) animation.currentTime = phaseRef.current * duration\n    // Stopped here rather than left to the effect below, which runs a paint later: a marquee\n    // mounting under a pointer that is already over it would otherwise move for one frame.\n    if (stoppedRef.current) animation.pause()\n    appliedRef.current = stoppedRef.current\n    liveRef.current = { animation, duration, signature }\n\n    return release\n  }, [geometry, reduced, speed, direction, release])\n\n  React.useEffect(() => {\n    const animation = liveRef.current?.animation\n    if (!animation) return\n    // Only on a change, and a freshly built animation counts as already applied — it was created in\n    // the state it was wanted in. Asking a running animation to play again is harmless, but \"only\n    // when it changes\" is the rule being described and it belongs in the component rather than in a\n    // dependency array that a test harness is free to ignore.\n    if (appliedRef.current === stopped) return\n    appliedRef.current = stopped\n    // pause() holds the transform where it is and play() carries on from there rather than from the\n    // start — measured resuming at 806ms from a pause taken at 700ms — which is the whole reason\n    // hover can stop this without losing the reader's place.\n    if (stopped) animation.pause()\n    else animation.play()\n  }, [stopped])\n\n  const toggleHeld = () => {\n    const next = !held\n    if (paused === undefined) setHeldInternally(next)\n    onPausedChange?.(next)\n  }\n\n  if (reduced) {\n    return (\n      // The five handlers are passed back one by one because they were destructured out of `props`\n      // for the branch below, where each is wrapped. Leaving them out here would drop a caller's own\n      // onFocus or onPointerEnter for exactly the visitors who asked for less motion, which is the\n      // kind of hole that no amount of looking at the moving version would show.\n      <div\n        ref={viewportRef}\n        className={cn(\"overflow-hidden\", className)}\n        style={style}\n        onPointerEnter={onPointerEnter}\n        onPointerLeave={onPointerLeave}\n        onPointerCancel={onPointerCancel}\n        onFocus={onFocus}\n        onBlur={onBlur}\n        {...props}\n      >\n        {/*\n          One copy, allowed to wrap, and nothing in here refusing to shrink. The animated tree cannot\n          be reused with the animation switched off: its copies are `shrink-0`, which is what lets\n          the track be wider than the box at all, and a row that will not shrink does not wrap —\n          measured, the same six items in a 300px box overflowed it by 233px either way until the\n          `shrink-0` came off, at which point they wrapped to three rows with nothing cut.\n        */}\n        <div className=\"flex flex-wrap items-center\" style={{ gap }}>\n          {children}\n        </div>\n      </div>\n    )\n  }\n\n  return (\n    <div\n      ref={viewportRef}\n      className={cn(\"group relative overflow-hidden\", className)}\n      style={{\n        // `clip` rather than `hidden`, because `hidden` makes this a scroll container and a\n        // scrolled one is a broken one: the track is positioned against this box, so any scroll\n        // offset slides the band sideways and the animation keeps running from the wrong place.\n        // Nothing asks it to scroll except the browser itself — measured, tabbing to the last link\n        // of the visible copy in a box narrower than that copy scrolled this box to 284 and left it\n        // there. `clip` is the same clipping without the scroll container: the same focus left it\n        // at 0, and did not scroll the page either. The class above stays as the fallback, since an\n        // engine that does not know `clip` drops this declaration and keeps `hidden`.\n        overflow: \"clip\",\n        ...style,\n      }}\n      // Whether anything is moving, for a caller who wants to style the stopped state and for\n      // anyone asking the question from the outside. The three ways to stop this — the button, a\n      // hovering pointer, focus landing inside — are one state as far as the animation is concerned,\n      // and this is that state rather than any one of its causes.\n      data-state={stopped ? \"paused\" : \"running\"}\n      onPointerEnter={(event) => {\n        // Touch is left out on purpose. A tap fires pointerenter with no pointer left to leave\n        // afterwards, and a band stopped for the rest of a visit because a thumb brushed a logo is\n        // worse than one that never stopped. A touch visitor gets the button, which is visible from\n        // the start where there is no hover.\n        if (event.pointerType !== \"touch\") setHovered(true)\n        onPointerEnter?.(event)\n      }}\n      onPointerLeave={(event) => {\n        setHovered(false)\n        onPointerLeave?.(event)\n      }}\n      onPointerCancel={(event) => {\n        setHovered(false)\n        onPointerCancel?.(event)\n      }}\n      onFocus={(event) => {\n        setFocusWithin(true)\n        onFocus?.(event)\n      }}\n      onBlur={(event) => {\n        setFocusWithin(false)\n        onBlur?.(event)\n      }}\n      {...props}\n    >\n      {/*\n        Sizing, and nothing else: out of sight, out of the accessibility tree, and inert so its copy\n        of any link is not a tab stop.\n\n        It is here because the track cannot be allowed to size this box. The track is as wide as the\n        copy count, so a box taking its width from its content gets wider every time the count goes\n        up, and a wider box needs a higher count — measured inside an `inline-flex` parent, that ran\n        210px → 444px → 912px → 1848px as the count doubled, with nothing to stop it. With the track\n        out of flow and one copy's worth of content in flow, the width came out identical at 1, 2, 4,\n        8 and 16 copies in all four parent layouts tried.\n\n        The height is the same bargain from the other side: an absolutely positioned track leaves\n        none, and taking the height from a measurement would mean the box is the wrong height until\n        JavaScript has run. This way it is right in the server's HTML. `w-max` rather than the\n        default is load-bearing for that — a spacer narrow enough to squeeze its items reported 60px\n        of height where the copies were 24px, because squeezed items wrap and wrapped items are\n        taller.\n      */}\n      <div\n        ref={spacerRef}\n        className=\"invisible flex w-max items-center\"\n        aria-hidden=\"true\"\n        inert\n        style={{ gap }}\n      >\n        {children}\n      </div>\n      <div\n        ref={trackRef}\n        className=\"absolute inset-y-0 left-0 flex w-max items-center\"\n        style={{\n          gap,\n          // Where the animation will hold it, applied for the frame between measuring the distance\n          // and starting the animation that uses it. Travelling rightwards starts a step to the\n          // left, so without this the track sits at 0 for a frame and then jumps. An animation wins\n          // over an inline transform — measured, −456 against the −912 written here — so this is\n          // only ever the resting position.\n          transform:\n            direction === \"right\" && geometry.step > 0\n              ? `translateX(${-geometry.step}px)`\n              : undefined,\n        }}\n      >\n        {Array.from({ length: geometry.copies }, (_, index) => (\n          <div\n            key={index}\n            // shrink-0 is what makes the seam possible: each copy keeps its own width inside a track\n            // wider than the box, rather than being squeezed to fit it.\n            className=\"flex shrink-0 items-center\"\n            aria-hidden={index === 0 ? undefined : \"true\"}\n            inert={index === 0 ? undefined : true}\n            style={{ gap }}\n          >\n            {children}\n          </div>\n        ))}\n      </div>\n      {controls ? (\n        <button\n          type=\"button\"\n          onClick={toggleHeld}\n          aria-label={held ? resumeLabel : pauseLabel}\n          // Out of the way until it is wanted, but never out of reach: keyboard focus reveals it,\n          // and on a device with no hover to reveal it with it is visible from the start. It is the\n          // only way to stop this without a pointer, so it cannot be the hover-only affordance that\n          // code-block's copy button can afford to be.\n          className=\"absolute right-2 top-1/2 inline-flex h-7 w-7 -translate-y-1/2 items-center justify-center rounded-md border border-input bg-background/80 text-foreground opacity-0 backdrop-blur-sm transition-opacity hover:bg-accent hover:text-accent-foreground focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring group-hover:opacity-100 [@media(hover:none)]:opacity-100\"\n        >\n          {held ? (\n            <Play className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />\n          ) : (\n            <Pause className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />\n          )}\n        </button>\n      ) : null}\n    </div>\n  )\n})\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui",
  "docs": "Install any pulld component by name: add \"@pulld\": \"https://pulld.pages.dev/r/{name}.json\" to the registries block in components.json, then `npx shadcn add @pulld/<name>`. All 140 components: https://pulld.pages.dev/?utm_source=cli"
}
