{"$schema":"https://ui.artbloom.tech/schema/registry-item.json","name":"marquee","type":"registry:ui","title":"Marquee","description":"Seamless infinite scroller, horizontal or vertical, with edge fades and pause-on-hover.","author":"@artbloom","dependencies":["clsx@2.1.1","tailwind-merge@3.6.0"],"registryDependencies":[],"files":[{"path":"components/ui/marquee.tsx","target":"components/ui/marquee.tsx","content":"\"use client\"\n\nimport type { CSSProperties, ReactNode } from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport type MarqueeProps = {\n  children: ReactNode\n  className?: string\n  /** Scroll right-to-left (default) or left-to-right. */\n  reverse?: boolean\n  /** Seconds for one full pass. */\n  duration?: number\n  /** Gap between repetitions, any CSS length. */\n  gap?: string\n  vertical?: boolean\n  /** How many copies to render. 2 is enough for a seamless loop. */\n  repeat?: number\n  pauseOnHover?: boolean\n  /** Fade the leading and trailing edges into the background. */\n  fade?: boolean\n}\n\n/**\n * Seamless infinite scroller. The track is duplicated `repeat` times and shifted\n * by exactly one copy's width, so the seam never lands mid-frame.\n *\n * Requires the `marquee-x` / `marquee-y` keyframes in your global stylesheet.\n *\n * Under `prefers-reduced-motion` the track is paused rather than removed. It\n * holds at 0%, which is the position the content would occupy with no animation\n * at all, so nothing moves and nothing is hidden. The pause is a class and the\n * animation is an inline style, so they set different properties and neither has\n * to out-specify the other.\n */\nexport function Marquee({\n  children,\n  className,\n  reverse = false,\n  duration = 30,\n  gap = \"3rem\",\n  vertical = false,\n  repeat = 2,\n  pauseOnHover = false,\n  fade = true,\n}: MarqueeProps) {\n  return (\n    <div\n      className={cn(\n        \"group relative flex overflow-hidden\",\n        vertical ? \"h-full flex-col\" : \"w-full flex-row\",\n        className,\n      )}\n      style={{ \"--marquee-gap\": gap, gap } as CSSProperties}\n    >\n      {Array.from({ length: repeat }, (_, i) => (\n        <div\n          key={i}\n          aria-hidden={i > 0}\n          className={cn(\n            \"flex shrink-0 items-center justify-around\",\n            \"motion-reduce:[animation-play-state:paused]\",\n            vertical ? \"flex-col\" : \"flex-row\",\n            pauseOnHover && \"group-hover:[animation-play-state:paused]\",\n          )}\n          style={{\n            gap,\n            animationName: vertical ? \"marquee-y\" : \"marquee-x\",\n            animationDuration: `${duration}s`,\n            animationTimingFunction: \"linear\",\n            animationIterationCount: \"infinite\",\n            animationDirection: reverse ? \"reverse\" : \"normal\",\n          }}\n        >\n          {children}\n        </div>\n      ))}\n\n      {fade && (\n        <>\n          {/*\n           * The fade is a share of the track, not a fixed 96px. At hero width the two\n           * behave the same — 12% of 1340px clamps straight back to 6rem — but a\n           * marquee dropped into a sidebar or a 298px card had 192px of gradient over\n           * 296px of content, so two thirds of every name was under a vignette and the\n           * rest read as clipped. A percentage keeps the edge soft at any width the\n           * component is actually given.\n           */}\n          <div\n            aria-hidden\n            className={cn(\n              \"pointer-events-none absolute from-background to-transparent\",\n              vertical\n                ? \"inset-x-0 top-0 h-[clamp(1.5rem,12%,6rem)] bg-gradient-to-b\"\n                : \"inset-y-0 left-0 w-[clamp(1.5rem,12%,6rem)] bg-gradient-to-r\",\n            )}\n          />\n          <div\n            aria-hidden\n            className={cn(\n              \"pointer-events-none absolute from-background to-transparent\",\n              vertical\n                ? \"inset-x-0 bottom-0 h-[clamp(1.5rem,12%,6rem)] bg-gradient-to-t\"\n                : \"inset-y-0 right-0 w-[clamp(1.5rem,12%,6rem)] bg-gradient-to-l\",\n            )}\n          />\n        </>\n      )}\n    </div>\n  )\n}\n","type":"registry:ui"},{"path":"lib/utils.ts","target":"lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n","type":"registry:lib"}],"css":{"@keyframes marquee-x":{"from":{"transform":"translateX(0)"},"to":{"transform":"translateX(calc(-100% - var(--marquee-gap, 3rem)))"}},"@keyframes marquee-y":{"from":{"transform":"translateY(0)"},"to":{"transform":"translateY(calc(-100% - var(--marquee-gap, 3rem)))"}}},"meta":{"kind":"animations","categories":["marquees","infinite"],"docs":"https://ui.artbloom.tech/artbloom/animations/marquee"}}