{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "marquee",
  "type": "registry:ui",
  "title": "Marquee",
  "description": "An infinitely scrolling strip: it renders its children `repeat` times side by side (or stacked, with `vertical`) inside an `overflow-hidden` track and translates each copy by its own width plus the gap, so the row reads as continuous.",
  "author": "ofri-peretz <https://github.com/ofri-peretz>",
  "categories": [
    "decorative",
    "effect"
  ],
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "https://ds.interlace.tools/r/theme.json",
    "https://ds.interlace.tools/r/cn.json",
    "https://ds.interlace.tools/r/use-reduced-motion.json"
  ],
  "files": [
    {
      "path": "registry/interlace-ui/magicui/marquee.tsx",
      "target": "components/ui/magicui/marquee.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport { ComponentPropsWithoutRef, useState } from \"react\";\n\n// @interlace/marquee v1.2.0 — Interlace design system.\n// Docs, props and live preview: https://ds.interlace.tools/c/marquee\n// What changed since: https://ds.interlace.tools/c/marquee#history\n// Generated banner — keep it, the upgrade diff reads this version.\n\n/**\n * @interlace/ui — Marquee\n *\n * An infinitely scrolling strip: it renders its children `repeat` times side\n * by side (or stacked, with `vertical`) inside an `overflow-hidden` track and\n * translates each copy by its own width plus the gap, so the row reads as\n * continuous.\n *\n * Logo walls, testimonial rails, ticker strips.\n *\n * Our reimplementation of the Magic UI component of the same name. What is\n * ours: the visible, Tab-reachable pause button and the reduced-motion gate.\n *\n * ## Anatomy\n *\n *   div.relative\n *     ├─ div                         (track — overflow-hidden, --duration:40s,\n *     │                               --gap:1rem, flex-row or flex-col)\n *     │   └─ div ×repeat             (.animate-marquee / -vertical, each\n *     │                               carrying the same children)\n *     └─ button                      (play/pause, aria-pressed, top-right)\n *\n * ## Motion\n *\n * A CSS keyframe (`--animate-marquee` in `styles/tokens.css`) covered three\n * ways under `prefers-reduced-motion: reduce`: the `animation: none` block in\n * `tokens.css` names both marquee classes, the wildcard in `preflight.css`\n * clamps the duration, and this component reads the preference in JS and adds\n * `[animation-play-state:paused]`. Nothing depends on a single layer.\n *\n * Two things the layering decides:\n *\n * - `isAnimating` is `!paused && !reducedMotion`: reduced motion outranks the\n *   click, so the button could never resume under `reduce`. It is therefore\n *   not rendered at all there (`showControl`), rather than shipped as a \"Play\"\n *   control that does nothing. Same call as `AnimatedList`.\n * - `pauseOnHover` is a `group-hover:` class on the track, so it pauses on\n *   pointer only. The button is the keyboard path, which is why\n *   `showPauseControl` defaults to `true` — WCAG 2.2.2 (Pause, Stop, Hide)\n *   applies to anything auto-scrolling for more than five seconds, and the\n *   default duration is 40.\n */\n\nimport { Pause, Play } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useReducedMotion } from \"@/hooks/use-reduced-motion\";\n\ninterface MarqueeProps extends ComponentPropsWithoutRef<\"div\"> {\n  /**\n   * Optional CSS class name to apply custom styles\n   */\n  className?: string;\n  /**\n   * Whether to reverse the animation direction\n   * @default false\n   */\n  reverse?: boolean;\n  /**\n   * Whether to pause the animation on hover\n   * @default false\n   */\n  pauseOnHover?: boolean;\n  /**\n   * Content to be displayed in the marquee\n   */\n  children: React.ReactNode;\n  /**\n   * Whether to animate vertically instead of horizontally\n   * @default false\n   */\n  vertical?: boolean;\n  /**\n   * Number of times to repeat the content\n   * @default 2\n   */\n  repeat?: number;\n  /**\n   * Render a visible play/pause button (WCAG 2.2.2 compliance for any\n   * marquee that runs longer than 5 seconds). Defaults to true — turn off\n   * only when the marquee is wrapped by another control surface that also\n   * exposes pause (e.g. a dashboard widget with its own toolbar).\n   * @default true\n   */\n  showPauseControl?: boolean;\n  /**\n   * Accessible label for the pause/play control. Customize when the marquee\n   * has a specific role (e.g. \"Pause sponsor logos\").\n   * @default \"Pause scrolling content\"\n   */\n  pauseLabel?: string;\n}\n\n/**\n * Marquee — see the file header for the motion contract and its edges.\n */\nexport function Marquee({\n  className,\n  reverse = false,\n  pauseOnHover = false,\n  children,\n  vertical = false,\n  repeat = 2, // Performance: Reduced from 4 to 2 (50% fewer DOM nodes)\n  showPauseControl = true,\n  pauseLabel = \"Pause scrolling content\",\n  ...props\n}: MarqueeProps) {\n  const reducedMotion = useReducedMotion();\n  const [paused, setPaused] = useState(false);\n  // The animation is \"running\" iff the user hasn't explicitly paused it AND\n  // they don't have reduced-motion preference. Reduced motion overrides the\n  // user's button click — anything else would re-animate when they don't want it.\n  const isAnimating = !paused && !reducedMotion;\n  // …and because `reduce` outranks the click, the control is withdrawn rather\n  // than rendered inert. WCAG 2.2.2 asks for a mechanism to pause auto-updating\n  // content; under `reduce` nothing is updating, so there is nothing to offer —\n  // and a \"Play\" button that cannot play is a worse answer than no button.\n  // Matches `AnimatedList`.\n  const showControl = showPauseControl && !reducedMotion;\n\n  return (\n    <div className=\"relative\">\n      <div\n        {...props}\n        className={cn(\n          \"group flex gap-(--gap) overflow-hidden p-2 [--duration:40s] [--gap:1rem]\",\n          {\n            \"flex-row\": !vertical,\n            \"flex-col\": vertical,\n          },\n          className,\n        )}\n      >\n        {Array(repeat)\n          .fill(0)\n          .map((_, i) => (\n            <div\n              key={i}\n              className={cn(\"flex shrink-0 justify-around gap-(--gap)\", {\n                \"animate-marquee flex-row\": !vertical,\n                \"animate-marquee-vertical flex-col\": vertical,\n                \"group-hover:[animation-play-state:paused]\": pauseOnHover,\n                \"[animation-direction:reverse]\": reverse,\n                \"[animation-play-state:paused]\": !isAnimating,\n              })}\n            >\n              {children}\n            </div>\n          ))}\n      </div>\n      {showControl && (\n        <button\n          type=\"button\"\n          onClick={() => setPaused((p) => !p)}\n          aria-label={isAnimating ? pauseLabel : `Resume ${pauseLabel.toLowerCase().replace(/^pause /, \"\")}`}\n          aria-pressed={!isAnimating}\n          className=\"absolute right-2 top-2 z-10 inline-flex size-9 items-center justify-center rounded-full border border-border bg-background/80 text-foreground backdrop-blur-sm transition-colors hover:bg-accent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring\"\n        >\n          {isAnimating ? (\n            <Pause className=\"size-4\" aria-hidden=\"true\" />\n          ) : (\n            <Play className=\"size-4\" aria-hidden=\"true\" />\n          )}\n        </button>\n      )}\n    </div>\n  );\n}\n"
    }
  ],
  "meta": {
    "tier": "effect",
    "client": true,
    "minViewport": null,
    "loading": false,
    "version": "1.2.0",
    "since": "1.0.0"
  },
  "docs": "## @interlace/marquee\n\nInstalled to `components/ui/magicui/marquee.tsx`.\n\n```tsx\nimport { /* … */ } from '@/components/ui/magicui/marquee';\n```\n\nProps, a11y contract, live preview and source: https://ds.interlace.tools/c/marquee\n\nRequires the `@interlace/theme` CSS baseline (installed automatically as a registry dependency)."
}
