shadimage
Billingsdk
blockbannersource: ok · hit

Top Banner

A banner component

npx shadcn add "https://billingsdk.com/r/banner.json"
free

Where it goes

banner

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · medium
iconslucide-react
animationmotion

Registry dependencies

buttonutils

Files2 · source cached 10/9/2026, 6:08:09 AM

src/registry/billingsdk/banner.tsx
"use client";

import type React from "react";

import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { X } from "lucide-react";
import { useState, useEffect } from "react";
import { motion, AnimatePresence } from "motion/react";

export interface BannerProps {
  variant?:
    | "default"
    | "minimal"
    | "popup"
    | "destructive"
    | "warning"
    | "success"
    | "info"
    | "announcement";
  title: string;
  description?: string;
  buttonText?: string;
  buttonIcon?: React.ReactNode;
  buttonLink?: string;
  dismissable?: boolean;
  className?: string;
  autoDismiss?: number; // in ms
  onDismiss?: () => void;
  gradientColors?: string[];
}

export function Banner({
  variant = "default",
  title,
  description,
  buttonText,
  buttonIcon,
  buttonLink,
  dismissable = true,
  className,
  autoDismiss,
  onDismiss,
  gradientColors,
}: BannerProps) {
  const [isVisible, setIsVisible] = useState(true);

  useEffect(() => {
    if (isVisible && autoDismiss) {
      const timer = setTimeout(() => handleDismiss(), autoDismiss);
      return () => clearTimeout(timer);
    }
  }, [isVisible, autoDismiss]);

  const handleDismiss = () => {
    setIsVisible(false);
    onDismiss?.();
  };

  const getVariantStyles = () => {
    const hasGradient = gradientColors && gradientColors.length > 0;

    switch (variant) {
      case "minimal":
        return {
          container: hasGradient
            ? "sticky top-0 z-50 w-full border-b backdrop-blur supports-[backdrop-filter]:bg-card/60"
            : "sticky top-0 z-50 w-full border-b bg-card/95 backdrop-blur supports-[backdrop-filter]:bg-card/60",
          wrapper:
            "relative container mx-auto flex flex-col sm:flex-row md:items-start items-center justify-center px-3 sm:px-4 py-2 gap-2 sm:gap-4 max-w-2xl",
          content:
            "flex flex-col sm:flex-row items-start sm:items-center gap-1 sm:gap-2",
          title: "text-sm font-medium text-card-foreground leading-tight",
          description: "text-xs text-muted-foreground sm:ml-2",
          actions:
            "flex items-center justify-center md:justify-start gap-2 sm:self-auto",
        };
      case "popup":
        return {
          container: hasGradient
            ? "fixed top-4 left-1/2 -translate-x-1/2 z-50 max-w-sm sm:max-w-md w-[90%] sm:w-auto border border-border rounded-lg shadow-lg backdrop-blur"
            : "fixed top-4 left-1/2 -translate-x-1/2 z-50 max-w-sm sm:max-w-md w-[90%] sm:w-auto bg-popover border border-border rounded-lg shadow-lg backdrop-blur",
          wrapper:
            "relative flex flex-col sm:flex-row items-start sm:items-center justify-between px-3 sm:px-4 py-3 gap-3 sm:gap-4",
          content:
            "flex flex-col sm:flex-row items-start sm:items-center gap-2 flex-1",
          title: "text-sm font-medium text-popover-foreground leading-snug",
          description: "text-xs text-muted-foreground",
          actions:
            "flex items-center justify-center md:justify-start gap-2 sm:self-auto flex-shrink-0 pr-12",
        };
      case "destructive":
        return {
          container: hasGradient
            ? "sticky top-0 z-50 w-full border-b border-destructive/20 backdrop-blur supports-[backdrop-filter]:bg-destructive/10"
            : "sticky top-0 z-50 w-full border-b bg-destructive text-destructive-foreground shadow-sm backdrop-blur",
          wrapper:
            "relative container mx-auto flex flex-col sm:flex-row md:items-start items-center justify-between px-3 sm:px-4 py-2 sm:py-3 gap-2 sm:gap-4",
          content:
            "flex flex-col sm:flex-row md:items-start items-center gap-2 w-full",
          title: hasGradient
            ? "text-sm font-medium text-destructive leading-tight"
            : "text-sm font-medium text-destructive-foreground leading-tight",
          description: hasGradient
            ? "text-xs text-destructive/80"
            : "text-xs text-destructive-foreground/80",
          actions:
            "flex items-center justify-center md:justify-start gap-2 sm:self-auto pr-12",
        };
      case "warning":
        return {
          container: hasGradient
            ? "sticky top-0 z-50 w-full border-b border-yellow-500/20 backdrop-blur supports-[backdrop-filter]:bg-yellow-50/60 dark:supports-[backdrop-filter]:bg-yellow-950/60"
            : "sticky top-0 z-50 w-full border-b bg-yellow-50 dark:bg-yellow-950 border-yellow-200 dark:border-yellow-800 shadow-sm backdrop-blur",
          wrapper:
            "relative container mx-auto flex flex-col sm:flex-row md:items-start items-center justify-between px-3 sm:px-4 py-2 sm:py-3 gap-2 sm:gap-4",
          content:
            "flex flex-col sm:flex-row md:items-start items-center gap-2 w-full",
          title: hasGradient
            ? "text-sm font-medium text-yellow-800 dark:text-yellow-200 leading-tight"
            : "text-sm font-medium text-yellow-800 dark:text-yellow-200 leading-tight",
          description: hasGradient
            ? "text-xs text-yellow-700 dark:text-yellow-300"
            : "text-xs text-yellow-700 dark:text-yellow-300",
          actions:
            "flex items-center justify-center md:justify-start gap-2 sm:self-auto pr-12",
        };
      case "success":
        return {
          container: hasGradient
            ? "sticky top-0 z-50 w-full border-b border-green-500/20 backdrop-blur supports-[backdrop-filter]:bg-green-50/60 dark:supports-[backdrop-filter]:bg-green-950/60"
            : "sticky top-0 z-50 w-full border-b bg-green-50 dark:bg-green-950 border-green-200 dark:border-green-800 shadow-sm backdrop-blur",
          wrapper:
            "relative container mx-auto flex flex-col sm:flex-row md:items-start items-center justify-between px-3 sm:px-4 py-2 sm:py-3 gap-2 sm:gap-4",
          content:
            "flex flex-col sm:flex-row md:items-start items-center gap-2 w-full",
          title: hasGradient
            ? "text-sm font-medium text-green-800 dark:text-green-200 leading-tight"
            : "text-sm font-medium text-green-800 dark:text-green-200 leading-tight",
          description: hasGradient
            ? "text-xs text-green-700 dark:text-green-300"
            : "text-xs text-green-700 dark:text-green-300",
          actions:
            "flex items-center justify-center md:justify-start gap-2 sm:self-auto pr-12",
        };
      case "info":
        return {
          container: hasGradient
            ? "sticky top-0 z-50 w-full border-b border-blue-500/20 backdrop-blur supports-[backdrop-filter]:bg-blue-50/60 dark:supports-[backdrop-filter]:bg-blue-950/60"
            : "sticky top-0 z-50 w-full border-b bg-blue-50 dark:bg-blue-950 border-blue-200 dark:border-blue-800 shadow-sm backdrop-blur",
          wrapper:
            "relative container mx-auto flex flex-col sm:flex-row md:items-start items-center justify-between px-3 sm:px-4 py-2 sm:py-3 gap-2 sm:gap-4",
          content:
            "flex flex-col sm:flex-row md:items-start items-center gap-2 w-full",
          title: hasGradient
            ? "text-sm font-medium text-blue-800 dark:text-blue-200 leading-tight"
            : "text-sm font-medium text-blue-800 dark:text-blue-200 leading-tight",
          description: hasGradient
            ? "text-xs text-blue-700 dark:text-blue-300"
            : "text-xs text-blue-700 dark:text-blue-300",
          actions:
            "flex items-center justify-center md:justify-start gap-2 sm:self-auto pr-12",
        };
      case "announcement":
        return {
          container: hasGradient
            ? "sticky top-0 z-50 w-full border-b border-purple-500/20 backdrop-blur supports-[backdrop-filter]:bg-purple-50/60 dark:supports-[backdrop-filter]:bg-purple-950/60"
            : "sticky top-0 z-50 w-full border-b bg-purple-50 dark:bg-purple-950 border-purple-200 dark:border-purple-800 shadow-sm backdrop-blur",
          wrapper:
            "relative container mx-auto flex flex-col sm:flex-row md:items-start items-center justify-between px-3 sm:px-4 py-2 sm:py-3 gap-2 sm:gap-4",
          content:
            "flex flex-col sm:flex-row md:items-start items-center gap-2 w-full",
          title: hasGradient
            ? "text-sm font-medium text-purple-800 dark:text-purple-200 leading-tight"
            : "text-sm font-medium text-purple-800 dark:text-purple-200 leading-tight",
          description: hasGradient
            ? "text-xs text-purple-700 dark:text-purple-300"
            : "text-xs text-purple-700 dark:text-purple-300",
          actions:
            "flex items-center justify-center md:justify-start gap-2 sm:self-auto pr-12",
        };
      default:
        return {
          container: hasGradient
            ? "sticky top-0 z-50 w-full border-b text-primary-foreground shadow-sm text-left backdrop-blur"
            : "sticky top-0 z-50 w-full border-b bg-primary text-primary-foreground shadow-sm text-left backdrop-blur",
          wrapper:
            "relative container mx-auto flex flex-col sm:flex-row md:items-start items-center justify-between px-3 sm:px-4 py-2 sm:py-3 gap-2 sm:gap-4",
          content:
            "flex flex-col text-center sm:text-left sm:flex-row md:items-start items-center gap-2 w-full",
          title: hasGradient
            ? "text-sm font-medium text-foreground leading-tight"
            : "text-sm font-medium text-primary-foreground leading-tight",
          description: hasGradient
            ? "text-xs text-foreground/80"
            : "text-xs text-primary-foreground/80",
          actions:
            "flex items-center justify-center md:justify-start gap-2 sm:self-auto pr-12",
        };
    }
  };

  const styles = getVariantStyles();

  const getGradientBackground = () => {
    if (!gradientColors || gradientColors.length === 0) return null;

    // Use the exact gradient from the example or custom colors with proper spacing
    let gradientStops;
    if (gradientColors.length === 4) {
      // Match the original example exactly
      gradientStops = `${gradientColors[0]} 0%, ${gradientColors[1]} 12.5%, ${gradientColors[2]} 25%, ${gradientColors[3]} 37.5%, ${gradientColors[0]} 50%`;
    } else {
      // For other numbers of colors, use equal spacing
      gradientStops = gradientColors
        .map((color, index) => {
          const percentage = (index / gradientColors.length) * 100;
          return `${color} ${percentage}%`;
        })
        .join(", ");
    }

    // Use consistent filter for better visibility
    const filterValue = "saturate(1.8) brightness(1.2)";

    return (
      <div
        className="absolute inset-0 z-[-1]"
        style={{
          maskImage:
            "linear-gradient(to bottom, white, transparent), radial-gradient(circle at top center, white, transparent)",
          maskComposite: "intersect",
          animation: "fd-moving-banner 30s linear infinite",
          backgroundImage: `repeating-linear-gradient(70deg, ${gradientStops})`,
          backgroundSize: "200% 100%",
          filter: filterValue,
        }}
      />
    );
  };

  const getAnimationProps = () => {
    switch (variant) {
      case "popup":
        return {
          initial: { opacity: 0, scale: 0.95, y: -20 },
          animate: { opacity: 1, scale: 1, y: 0 },
          exit: { opacity: 0, scale: 0.95, y: -20 },
        };
      default:
        return {
          initial: { opacity: 0, y: -12 },
          animate: { opacity: 1, y: 0 },
          exit: { opacity: 0, y: -12 },
        };
    }
  };

  return (
    <AnimatePresence>
      {isVisible && (
        <motion.div
          {...getAnimationProps()}
          transition={{ duration: 0.15, ease: "easeOut" }}
          className={cn(styles.container, className)}
        >
          {getGradientBackground()}
          <div className={styles.wrapper}>
            {/* Content */}
            <div className={styles.content}>
              <div
                className={
                  variant === "minimal"
                    ? "flex flex-col gap-0.5 md:flex-row md:gap-2"
                    : ""
                }
              >
                <p className={styles.title}>{title}</p>
                {description && (
                  <p className={styles.description}>{description}</p>
                )}
              </div>
            </div>

            {/* Actions */}
            <div className={styles.actions}>
              {buttonText && variant !== "minimal" && (
                <Button
                  variant={variant === "default" ? "secondary" : "default"}
                  size="sm"
                  onClick={() => window.open(buttonLink, "_blank")}
                  className={"h-8"}
                >
                  {buttonIcon && (
                    <div className="mr-1 flex-shrink-0">{buttonIcon}</div>
                  )}
                  {buttonText}
                </Button>
              )}
            </div>

            {/* Close button (always top-right) */}
            {dismissable && (
              <Button
                variant="ghost"
                size="icon"
                onClick={handleDismiss}
                className={cn(
                  "absolute top-2 right-1 h-8 w-8 sm:top-1/2 sm:right-2 sm:-translate-y-1/2",
                  gradientColors && gradientColors.length > 0
                    ? "hover:bg-foreground/20 text-foreground"
                    : variant === "default" &&
                        "hover:bg-primary-foreground/20 text-primary-foreground",
                  variant === "popup" &&
                    !gradientColors &&
                    "hover:bg-accent text-popover-foreground",
                  variant === "minimal" &&
                    !gradientColors &&
                    "hover:bg-accent text-card-foreground",
                  variant === "warning" &&
                    !gradientColors &&
                    "text-yellow-800 hover:bg-yellow-200 dark:text-yellow-200 dark:hover:bg-yellow-800",
                  variant === "success" &&
                    !gradientColors &&
                    "text-green-800 hover:bg-green-200 dark:text-green-200 dark:hover:bg-green-800",
                  variant === "info" &&
                    !gradientColors &&
                    "text-blue-800 hover:bg-blue-200 dark:text-blue-200 dark:hover:bg-blue-800",
                  variant === "announcement" &&
                    !gradientColors &&
                    "text-purple-800 hover:bg-purple-200 dark:text-purple-200 dark:hover:bg-purple-800",
                )}
              >
                <X className="h-4 w-4" />
                <span className="sr-only">Dismiss</span>
              </Button>
            )}
          </div>
        </motion.div>
      )}
    </AnimatePresence>
  );
}

// Add CSS keyframes for the moving banner animation
if (typeof document !== "undefined") {
  const styleSheet = document.createElement("style");
  styleSheet.textContent = `
    @keyframes fd-moving-banner {
      0% {
        background-position: 0% 50%;
      }
      50% {
        background-position: 100% 50%;
      }
      100% {
        background-position: 0% 50%;
      }
    }
  `;
  document.head.appendChild(styleSheet);
}
src/registry/billingsdk/demo/banner-demo.tsx
import { Banner } from "@/components/billingsdk/banner";

export default function FreeTrialBannerDemo() {
  return (
    <div className="bg-background-secondary flex h-full min-h-[500px] w-full flex-col gap-6 overflow-hidden rounded-lg border-2">
      <Banner
        title="🎉 Start your free trial today!"
        description="Get 30 days free access to all premium features"
        buttonText="Start Free Trial"
        buttonLink="https://example.com/signup"
        variant="default" // default, minimal, popup
      />

      {/* minimal hero example */}
      <section className="flex flex-col items-center justify-center gap-4 py-16 text-center">
        <h1 className="text-foreground-secondary text-3xl font-bold tracking-tight">
          Create next-generation digital products
        </h1>
        <div className="flex flex-col gap-2">
          <p className="text-muted-foreground max-w-md">
            Build faster with our platform
          </p>
          <a className="hover:text-primary underline underline-offset-4 transition">
            Get Started →
          </a>
        </div>
      </section>
    </div>
  );
}

More from Billingsdk

Browse all components in this library →