{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "marquee",
  "type": "registry:ui",
  "title": "Marquee",
  "description": "A scroll-velocity marquee with seamless loop math, eased hover pause, vertical/horizontal direction, ResizeObserver-based copy count, and full prefers-reduced-motion support.",
  "files": [
    {
      "path": "registry/sonaui/marquee/marquee.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport {\n  useAnimationFrame,\n  useMotionValue,\n  useReducedMotion,\n  useScroll,\n  useSpring,\n  useVelocity,\n} from \"motion/react\";\nimport { useEffect, useRef } from \"react\";\nimport { cn } from \"@/lib/sona-utils\";\n\nexport interface MarqueeProps {\n  /** Content of one marquee segment. Can be a single small element or a long strip. */\n  children: React.ReactNode;\n  className?: string;\n  /** Class for the outer clipping container. */\n  containerClassName?: string;\n  /**\n   * Scroll speed in pixels per second. Higher = faster.\n   * @default 80\n   */\n  speed?: number;\n  /**\n   * Gap between repeated segments, any CSS length string.\n   * @default \"4rem\"\n   */\n  gap?: string;\n  /**\n   * Scroll direction.\n   * @default \"left\"\n   */\n  direction?: \"left\" | \"right\" | \"up\" | \"down\";\n  /**\n   * Multiply speed by scroll velocity (and flip direction on scroll-up).\n   * @default false\n   */\n  scrollVelocity?: boolean;\n  /**\n   * Max speed multiplier when scrollVelocity is enabled.\n   * @default 5\n   */\n  maxVelocity?: number;\n  /**\n   * Pause (with easing, not a snap) on hover.\n   * @default false\n   */\n  pauseOnHover?: boolean;\n  /**\n   * How many segment copies to render.\n   * \"auto\" measures the container and fills 2× it.\n   * @default \"auto\"\n   */\n  repeat?: number | \"auto\";\n}\n\nexport default function Marquee({\n  children,\n  className,\n  containerClassName,\n  speed = 80,\n  gap = \"4rem\",\n  direction = \"left\",\n  scrollVelocity = false,\n  maxVelocity = 5,\n  pauseOnHover = false,\n  repeat = \"auto\",\n}: MarqueeProps) {\n  const shouldReduceMotion = useReducedMotion();\n\n  const containerRef = useRef<HTMLDivElement>(null);\n  const segmentRef = useRef<HTMLDivElement>(null);\n\n  // How many copies are rendered (updated by ResizeObserver)\n  const countRef = useRef<number>(typeof repeat === \"number\" ? repeat : 4);\n\n  // Motion value for translation — never stored in React state\n  const baseX = useMotionValue(0);\n  const isHovered = useRef(false);\n  // 0 = fully paused, 1 = full speed — lerped in rAF\n  const speedMultiplier = useRef(1);\n\n  // Scroll velocity pipeline (motion values only — no setState)\n  const { scrollY } = useScroll();\n  const scrollVelocityMV = useVelocity(scrollY);\n  const smoothVelocity = useSpring(scrollVelocityMV, {\n    damping: 50,\n    stiffness: 400,\n  });\n\n  const isVertical = direction === \"up\" || direction === \"down\";\n  const directionSign = direction === \"left\" || direction === \"up\" ? 1 : -1;\n\n  // ResizeObserver — recompute copy count when container or segment resize\n  useEffect(() => {\n    if (repeat !== \"auto\" || shouldReduceMotion) return;\n\n    const container = containerRef.current;\n    const segment = segmentRef.current;\n    if (!container || !segment) return;\n\n    function recompute() {\n      if (!container || !segment) return;\n      const containerSize = isVertical\n        ? container.offsetHeight\n        : container.offsetWidth;\n      const segmentSize = isVertical\n        ? segment.offsetHeight\n        : segment.offsetWidth;\n      if (segmentSize > 0) {\n        countRef.current = Math.max(\n          2,\n          Math.ceil((containerSize * 2) / segmentSize) + 1,\n        );\n      }\n    }\n\n    recompute();\n    const ro = new ResizeObserver(recompute);\n    ro.observe(container);\n    ro.observe(segment);\n    return () => ro.disconnect();\n  }, [repeat, isVertical, shouldReduceMotion]);\n\n  // Hover handlers\n  const onMouseEnter = () => {\n    if (pauseOnHover) isHovered.current = true;\n  };\n  const onMouseLeave = () => {\n    if (pauseOnHover) isHovered.current = false;\n  };\n\n  // Main animation loop — all in motion values, zero React state\n  useAnimationFrame((_, delta) => {\n    if (shouldReduceMotion) return;\n\n    const segment = segmentRef.current;\n    if (!segment) return;\n\n    const segmentSize = isVertical ? segment.offsetHeight : segment.offsetWidth;\n    if (segmentSize === 0) return;\n\n    // Lerp speedMultiplier toward target (eased pause on hover)\n    const targetMultiplier = isHovered.current ? 0 : 1;\n    speedMultiplier.current +=\n      (targetMultiplier - speedMultiplier.current) * 0.1;\n\n    // Velocity boost from scroll\n    let velocityBoost = 1;\n    let velocityFlip = 1;\n    if (scrollVelocity) {\n      const v = smoothVelocity.get();\n      velocityBoost = Math.min(Math.abs(v) / 200, maxVelocity);\n      velocityBoost = Math.max(velocityBoost, 1);\n      if (v < -50) velocityFlip = -1;\n    }\n\n    const pxPerMs = speed / 1000;\n    const delta_px =\n      pxPerMs *\n      delta *\n      directionSign *\n      velocityFlip *\n      speedMultiplier.current *\n      velocityBoost;\n\n    let next = baseX.get() - delta_px;\n\n    // Wrap: keep translation within [-segmentSize, 0)\n    if (directionSign > 0) {\n      // moving left/up — translate goes negative\n      if (next <= -segmentSize) next += segmentSize;\n    } else {\n      // moving right/down — translate goes positive\n      if (next >= 0) next -= segmentSize;\n      if (next < -segmentSize) next += segmentSize;\n    }\n\n    baseX.set(next);\n  });\n\n  const copies = repeat !== \"auto\" ? repeat : countRef.current;\n  const items = Array.from({ length: copies });\n\n  if (shouldReduceMotion) {\n    // Static strip — no animation\n    return (\n      <section\n        ref={containerRef}\n        aria-label=\"Scrolling content\"\n        className={cn(\"overflow-hidden\", containerClassName)}\n      >\n        <div\n          className={cn(\n            isVertical ? \"flex flex-col\" : \"flex flex-row\",\n            \"w-max\",\n            className,\n          )}\n          style={{ gap }}\n        >\n          <div ref={segmentRef}>{children}</div>\n          {items.map((_, i) => (\n            // biome-ignore lint/suspicious/noArrayIndexKey: static decorative copies\n            <div key={i} aria-hidden=\"true\">\n              {children}\n            </div>\n          ))}\n        </div>\n      </section>\n    );\n  }\n\n  return (\n    <section\n      ref={containerRef}\n      aria-label=\"Scrolling content\"\n      className={cn(\"overflow-hidden\", containerClassName)}\n      onMouseEnter={onMouseEnter}\n      onMouseLeave={onMouseLeave}\n    >\n      {/* The track: a flex row/col of copies, translated as a unit */}\n      <MotionTrack\n        baseX={baseX}\n        isVertical={isVertical}\n        gap={gap}\n        className={className}\n        segmentRef={segmentRef}\n        items={items}\n      >\n        {children}\n      </MotionTrack>\n    </section>\n  );\n}\n\n// Split into a separate component to isolate motion subscription\nimport { type MotionValue, motion } from \"motion/react\";\n\nfunction MotionTrack({\n  baseX,\n  isVertical,\n  gap,\n  className,\n  segmentRef,\n  items,\n  children,\n}: {\n  baseX: MotionValue<number>;\n  isVertical: boolean;\n  gap: string;\n  className?: string;\n  segmentRef: React.RefObject<HTMLDivElement | null>;\n  items: unknown[];\n  children: React.ReactNode;\n}) {\n  return (\n    <motion.div\n      style={isVertical ? { y: baseX } : { x: baseX }}\n      className={cn(\n        isVertical ? \"flex flex-col\" : \"flex flex-row\",\n        \"w-max will-change-transform\",\n        className,\n      )}\n    >\n      {/* First segment — measured for loop math */}\n      <div\n        ref={segmentRef}\n        style={{\n          paddingRight: isVertical ? 0 : gap,\n          paddingBottom: isVertical ? gap : 0,\n        }}\n      >\n        {children}\n      </div>\n      {/* Copies — decorative, aria-hidden */}\n      {items.map((_, i) => (\n        <div\n          key={String(i)}\n          aria-hidden=\"true\"\n          style={{\n            paddingRight: isVertical ? 0 : gap,\n            paddingBottom: isVertical ? gap : 0,\n          }}\n        >\n          {children}\n        </div>\n      ))}\n    </motion.div>\n  );\n}\n",
      "target": "components/ui/marquee/marquee.tsx"
    }
  ],
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "@sona-ui/sona-utils"
  ]
}