{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "scroll-velocity",
  "title": "Scroll Velocity",
  "description": "Marquee whose speed and direction follow your scrolling.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/components/velora/scroll-velocity.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef } from \"react\";\nimport {\n  motion,\n  useMotionValue,\n  useReducedMotion,\n  useScroll,\n  useTransform,\n  useVelocity,\n} from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\n\ninterface ScrollVelocityProps {\n  children: React.ReactNode;\n  /** Base speed in percent of content width per second */\n  baseVelocity?: number;\n  className?: string;\n}\n\n/**\n * A marquee whose speed and direction respond to how fast you are scrolling.\n * Holds a steady drift when the page is still, pauses while hovered or\n * focused, and stays still under `prefers-reduced-motion`.\n */\nexport function ScrollVelocity({\n  children,\n  baseVelocity = 4,\n  className,\n}: ScrollVelocityProps) {\n  const baseX = useMotionValue(0);\n  const direction = useRef(1);\n  const reducedMotion = useReducedMotion();\n  const hovered = useRef(false);\n  const focused = useRef(false);\n\n  const { scrollY } = useScroll();\n  const scrollVelocity = useVelocity(scrollY);\n  const velocityFactor = useTransform(scrollVelocity, [-1200, 1200], [-4, 4], {\n    clamp: false,\n  });\n\n  const root = useRef<HTMLDivElement>(null);\n\n  // Own rAF loop (not useAnimationFrame) so nothing ticks under reduced\n  // motion or while the marquee is offscreen.\n  useEffect(() => {\n    const el = root.current;\n    if (reducedMotion !== false || !el) return;\n    let frame = 0;\n    let last = 0;\n    let visible = false;\n\n    const tick = (now: number) => {\n      const delta = last ? now - last : 0;\n      last = now;\n      if (!hovered.current && !focused.current) {\n        let moveBy = direction.current * baseVelocity * (delta / 1000);\n        const factor = velocityFactor.get();\n        // Scrolling backwards flips the marquee's direction.\n        if (factor < 0) direction.current = -1;\n        else if (factor > 0) direction.current = 1;\n        moveBy += direction.current * moveBy * Math.abs(factor);\n        // One copy is 25% of the four-copy track, so wrap there.\n        baseX.set(((baseX.get() + moveBy) % 25) - 25);\n      }\n      frame = visible ? requestAnimationFrame(tick) : 0;\n    };\n\n    const observer = new IntersectionObserver(([entry]) => {\n      visible = entry.isIntersecting;\n      if (visible && !frame) {\n        last = 0;\n        frame = requestAnimationFrame(tick);\n      }\n    });\n    observer.observe(el);\n    return () => {\n      observer.disconnect();\n      cancelAnimationFrame(frame);\n    };\n  }, [reducedMotion, baseVelocity, baseX, velocityFactor]);\n\n  const x = useTransform(baseX, (value) => `${value}%`);\n\n  return (\n    <div\n      ref={root}\n      data-slot=\"scroll-velocity\"\n      onMouseEnter={() => (hovered.current = true)}\n      onMouseLeave={() => (hovered.current = false)}\n      onFocus={() => (focused.current = true)}\n      onBlur={(event) => {\n        if (!event.currentTarget.contains(event.relatedTarget as Node | null)) {\n          focused.current = false;\n        }\n      }}\n      className={cn(\"w-full overflow-hidden whitespace-nowrap\", className)}\n    >\n      <motion.div style={{ x }} className=\"flex w-max gap-8\">\n        {Array.from({ length: 4 }).map((_, i) => (\n          <span\n            key={i}\n            aria-hidden={i > 0 || undefined}\n            inert={i > 0}\n            className=\"shrink-0\"\n          >\n            {children}\n          </span>\n        ))}\n      </motion.div>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/velora/scroll-velocity.tsx"
    }
  ],
  "type": "registry:ui"
}