shadimage

Marquee

registry/designali/ui/marquee.tsx

import { ComponentPropsWithoutRef } from "react"

import { cn } from "@workspace/ui/lib/utils"

interface MarqueeProps extends ComponentPropsWithoutRef<"div"> {
  /**
   * Optional CSS class name to apply custom styles
   */
  className?: string
  /**
   * Whether to reverse the animation direction
   * @default false
   */
  reverse?: boolean
  /**
   * Whether to pause the animation on hover
   * @default false
   */
  pauseOnHover?: boolean
  /**
   * Content to be displayed in the marquee
   */
  children: React.ReactNode
  /**
   * Whether to animate vertically instead of horizontally
   * @default false
   */
  vertical?: boolean
  /**
   * Number of times to repeat the content
   * @default 5
   */
  repeat?: number
  /**
   * Animation speed variant or custom duration in seconds
   * @default "normal"
   */
  speed?: "slow" | "normal" | "fast" | number
  /**
   * Gap between repeated items (in pixels or any CSS unit)
   * @default "6px"
   */
  gap?: string | number
  /**
   * Apply fade effect at edges for smoother visual experience
   * @default true
   */
  fade?: boolean
  /**
   * Delay before animation starts (in seconds)
   * @default 0
   */
  delay?: number
  /**
   * Whether to apply auto-fill to calculate optimal repeat count
   * @default false
   */
  autoFill?: boolean
}

export function Marquee({
  className,
  reverse = false,
  pauseOnHover = false,
  children,
  vertical = false,
  repeat = 5,
  speed = "normal",
  gap = "6px",
  fade = false,
  delay = 0,
  autoFill = false,
  ...props
}: MarqueeProps) {
  const speedVariants = {
    slow: "[--duration:120s]",
    normal: "[--duration:40s]",
    fast: "[--duration:10s]",
  }

  const gapValue = typeof gap === "number" ? `${gap}px` : gap
  const duration = typeof speed === "number" ? `${speed}s` : undefined
  const repeatCount = autoFill ? 10 : repeat

  return (
    <div
      {...props}
      className={cn(
        "group relative flex overflow-hidden p-1",
        typeof speed === "string" ? speedVariants[speed] : "",
        {
          "flex-row": !vertical,
          "flex-col": vertical,
        },
        className
      )}
      style={
        {
          "--gap": gapValue,
          ...(duration && { "--duration": duration }),
          ...(delay && { "--delay": `${delay}s` }),
        } as React.CSSProperties
      }
    >
      {fade && (
        <>
          <div
            className={cn(
              "pointer-events-none absolute z-10",
              vertical
                ? "inset-x-0 top-0 h-1/6 bg-gradient-to-b from-background"
                : "inset-y-0 left-0 w-1/6 bg-gradient-to-r from-background"
            )}
          />
          <div
            className={cn(
              "pointer-events-none absolute z-10",
              vertical
                ? "inset-x-0 bottom-0 h-1/6 bg-gradient-to-t from-background"
                : "inset-y-0 right-0 w-1/6 bg-gradient-to-l from-background"
            )}
          />
        </>
      )}
      {Array(repeatCount)
        .fill(0)
        .map((_, i) => (
          <div
            key={i}
            className={cn("flex shrink-0 justify-around [gap:var(--gap)]", {
              "animate-marquee flex-row": !vertical,
              "animate-marquee-vertical flex-col": vertical,
              "group-hover:[animation-play-state:paused]": pauseOnHover,
              "[animation-direction:reverse]": reverse,
              "[animation-delay:var(--delay)]": delay > 0,
            })}
          >
            {children}
          </div>
        ))}
    </div>
  )
}
Interaction
marquee