{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"marquee","type":"registry:ui","title":"Marquee","description":"Items that scroll on their own and loop without a seam: logos, testimonials, integrations, a news or price ticker — sideways or up.","dependencies":[],"registryDependencies":["https://dashral.com/r/utils.json","https://dashral.com/r/lib-motion.json"],"files":[{"path":"registry/dashral/marquee.tsx","type":"registry:file","target":"components/dashral/marquee.tsx","content":"\"use client\"\n\nimport * as React from \"react\"\n\nimport { reducedMotion } from \"./lib/motion\"\nimport { cn } from \"./lib/utils\"\n\ntype MarqueeProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  /** The items, repeated as often as it takes to fill the strip. */\n  children: React.ReactNode\n  /** Pixels per second. */\n  speed?: number\n  /** Moves the other way: rightward (leftward in right-to-left text), or downward when vertical. */\n  reverse?: boolean\n  /** Scrolls up (or down, with reverse) instead of sideways. Give it a height. */\n  vertical?: boolean\n  /** Stops while the pointer is over it or focus is inside it. */\n  pauseOnHover?: boolean\n  /** Fades the items out at both ends. */\n  fade?: boolean\n  /** Space between items, and between one copy of them and the next: any CSS length. */\n  gap?: string\n  /** Names the strip for screen readers, e.g. \"Customers\". */\n  \"aria-label\"?: string\n  classNames?: { track?: string; group?: string }\n}\n\n/**\n * Items that scroll on their own, in one tag: logos, testimonials, news. They loop without a seam — the items are\n * repeated until the strip is full, and the strip moves by exactly one copy — at a steady speed in pixels per second,\n * sideways or up, and stop under the pointer if you ask. Copies after the first are hidden from screen readers and\n * the keyboard. With reduced motion it holds still and scrolls by hand. No CSS to add: it runs on the Web Animations\n * API.\n */\nfunction Marquee({\n  children,\n  speed = 40,\n  reverse = false,\n  vertical = false,\n  pauseOnHover = false,\n  fade = false,\n  gap = \"1rem\",\n  className,\n  classNames,\n  style,\n  ...props\n}: MarqueeProps) {\n  const root = React.useRef<HTMLDivElement>(null)\n  const track = React.useRef<HTMLDivElement>(null)\n  const first = React.useRef<HTMLDivElement>(null)\n  const animation = React.useRef<Animation | null>(null)\n  const [copies, setCopies] = React.useState(2)\n  const [still, setStill] = React.useState(false)\n\n  React.useEffect(() => {\n    const box = root.current\n    const strip = track.current\n    const group = first.current\n    if (!box || !strip || !group) return\n    if (reducedMotion()) {\n      setStill(true)\n      return\n    }\n\n    const start = () => {\n      animation.current?.cancel()\n      // One copy's length plus the gap after it: moving by that much lands on the next copy, exactly where it began.\n      const gapPx = parseFloat(getComputedStyle(strip)[vertical ? \"rowGap\" : \"columnGap\"]) || 0\n      const size = (vertical ? group.offsetHeight : group.offsetWidth) + gapPx\n      const room = vertical ? box.clientHeight : box.clientWidth\n      if (!size) return\n      // Enough copies to fill the strip and the stretch it moves through.\n      setCopies(Math.max(2, Math.ceil(room / size) + 1))\n      const rtl = !vertical && getComputedStyle(box).direction === \"rtl\"\n      const distance = rtl ? size : -size\n      const to = vertical ? `translateY(${distance}px)` : `translateX(${distance}px)`\n      animation.current = strip.animate([{ transform: \"translate(0, 0)\" }, { transform: to }], {\n        duration: (size / speed) * 1000,\n        iterations: Infinity,\n        easing: \"linear\",\n        direction: reverse ? \"reverse\" : \"normal\",\n      })\n    }\n\n    start()\n    // Measured again when the strip or its items change size (fonts loading, a resize).\n    const observer = new ResizeObserver(start)\n    observer.observe(box)\n    observer.observe(group)\n    return () => {\n      observer.disconnect()\n      animation.current?.cancel()\n    }\n    // The items' size is watched above, so new children don't restart the loop from the start.\n  }, [speed, reverse, vertical])\n\n  const hold = (paused: boolean) => {\n    if (!pauseOnHover || !animation.current) return\n    if (paused) animation.current.pause()\n    else animation.current.play()\n  }\n\n  const mask = fade\n    ? vertical\n      ? \"[mask-image:linear-gradient(to_bottom,transparent,black_12%,black_88%,transparent)]\"\n      : \"[mask-image:linear-gradient(to_right,transparent,black_10%,black_90%,transparent)]\"\n    : \"\"\n\n  return (\n    <div\n      ref={root}\n      data-slot=\"marquee\"\n      role={props[\"aria-label\"] ? \"region\" : undefined}\n      onPointerEnter={() => hold(true)}\n      onPointerLeave={() => hold(false)}\n      onFocus={() => hold(true)}\n      onBlur={(event) => {\n        if (!event.currentTarget.contains(event.relatedTarget)) hold(false)\n      }}\n      className={cn(\n        \"flex\",\n        vertical ? \"flex-col\" : \"flex-row\",\n        // Held still, it scrolls by hand instead; its scrollbar stays out of sight.\n        still ? (vertical ? \"overflow-y-auto\" : \"overflow-x-auto\") : \"overflow-hidden\",\n        \"[scrollbar-width:none] [&::-webkit-scrollbar]:hidden\",\n        mask,\n        className\n      )}\n      style={{ ...style, [\"--marquee-gap\" as string]: gap }}\n      {...props}\n    >\n      <div\n        ref={track}\n        data-slot=\"marquee-track\"\n        className={cn(\"flex shrink-0 gap-(--marquee-gap)\", vertical ? \"flex-col\" : \"flex-row\", classNames?.track)}\n      >\n        {Array.from({ length: still ? 1 : copies }, (_, i) => (\n          <div\n            key={i}\n            ref={i === 0 ? first : undefined}\n            data-slot=\"marquee-group\"\n            aria-hidden={i > 0 || undefined}\n            inert={i > 0}\n            className={cn(\"flex shrink-0 gap-(--marquee-gap)\", vertical ? \"flex-col\" : \"flex-row\", classNames?.group)}\n          >\n            {children}\n          </div>\n        ))}\n      </div>\n    </div>\n  )\n}\n\nexport { Marquee, type MarqueeProps }\n"}]}