# Urgency badge

Scarcity badge with a pulsing live dot and an optional percent sold bar that animates into view.

- Type: component
- Category: Feedback
- Tier: Free
- Dependencies: motion

## Install

```bash
npx shadcn@latest add https://facadeui.com/r/urgency-badge.json
```

## Usage

```tsx
import { UrgencyBadge } from "@/components/facade/urgency-badge";

export default function Page() {
  return <UrgencyBadge />;
}
```

## Source

```tsx
"use client";

import * as React from "react";
import { motion, useInView, useReducedMotion } from "motion/react";
import { cn } from "@/lib/utils";

export type UrgencyVariant = "neutral" | "brand" | "warning";

export interface UrgencyBadgeProps {
  /** Main message, e.g. "Only 3 homes left". */
  label?: React.ReactNode;
  /** Secondary line shown next to the progress bar, e.g. "Phase 2". */
  detail?: React.ReactNode;
  /** Percent sold (0 to 100). When set, a progress bar animates in. */
  progress?: number;
  /** Text after the percentage, defaults to "sold". */
  progressSuffix?: string;
  variant?: UrgencyVariant;
  /** Show the pulsing live dot. */
  pulse?: boolean;
  className?: string;
}

const variants: Record<UrgencyVariant, { root: string; dot: string; ping: string; bar: string; track: string }> = {
  neutral: {
    root: "border-border bg-card text-foreground",
    dot: "bg-success",
    ping: "bg-success/60",
    bar: "bg-foreground",
    track: "bg-muted",
  },
  brand: {
    root: "border-brand/25 bg-brand/10 text-foreground",
    dot: "bg-brand",
    ping: "bg-brand/60",
    bar: "bg-brand",
    track: "bg-brand/15",
  },
  warning: {
    root: "border-warning/40 bg-warning/15 text-foreground",
    dot: "bg-warning",
    ping: "bg-warning/70",
    bar: "bg-warning",
    track: "bg-warning/20",
  },
};

export function UrgencyBadge({
  label = "Only 3 homes left",
  detail,
  progress,
  progressSuffix = "sold",
  variant = "brand",
  pulse = true,
  className,
}: UrgencyBadgeProps) {
  const reduce = useReducedMotion();
  const ref = React.useRef<HTMLDivElement>(null);
  const inView = useInView(ref, { once: true, margin: "-40px 0px" });
  const v = variants[variant];
  const hasProgress = typeof progress === "number";
  const pct = hasProgress ? Math.min(100, Math.max(0, progress)) : 0;

  return (
    <div
      ref={ref}
      role="status"
      className={cn(
        "inline-flex flex-col gap-2 rounded-xl border px-3.5 py-2.5 text-sm shadow-sm",
        hasProgress ? "min-w-[240px]" : "rounded-full px-3 py-1.5",
        v.root,
        className,
      )}
    >
      <div className="flex items-center gap-2">
        {pulse && (
          <span className="relative flex size-2 shrink-0" aria-hidden="true">
            {!reduce && <span className={cn("absolute inline-flex size-full animate-ping-slow rounded-full", v.ping)} />}
            <span className={cn("relative inline-flex size-2 rounded-full", v.dot)} />
          </span>
        )}
        <span className="font-medium">{label}</span>
        {detail && !hasProgress && <span className="text-muted-foreground">· {detail}</span>}
      </div>
      {hasProgress && (
        <div className="space-y-1.5">
          <div
            className={cn("h-1.5 w-full overflow-hidden rounded-full", v.track)}
            role="progressbar"
            aria-valuemin={0}
            aria-valuemax={100}
            aria-valuenow={Math.round(pct)}
            aria-label={`${Math.round(pct)}% ${progressSuffix}`}
          >
            <motion.div
              className={cn("relative h-full rounded-full", v.bar)}
              initial={{ width: reduce ? `${pct}%` : "0%" }}
              animate={{ width: inView || reduce ? `${pct}%` : "0%" }}
              transition={{ duration: 1.2, ease: [0.22, 1, 0.36, 1], delay: 0.15 }}
            >
              {!reduce && (
                <span
                  className="absolute inset-0 animate-shimmer rounded-full opacity-40"
                  style={{
                    backgroundImage: "linear-gradient(90deg, transparent 0%, var(--background) 50%, transparent 100%)",
                    backgroundSize: "200% 100%",
                  }}
                  aria-hidden="true"
                />
              )}
            </motion.div>
          </div>
          <div className="flex items-center justify-between text-xs text-muted-foreground">
            <span>
              <span className="font-mono font-medium tabular-nums text-foreground">{Math.round(pct)}%</span> {progressSuffix}
            </span>
            {detail && <span>{detail}</span>}
          </div>
        </div>
      )}
    </div>
  );
}

export default UrgencyBadge;
```
