Climate

Offset Card

A carbon-offset project card with a verification badge, impact metrics, a funded-progress bar, and a per-tonne buy action.

Install

npx shadcn@latest add @paragon/offset-card

Also installs: button

offset-card.tsx

"use client";

import * as React from "react";
import { Leaf, MapPin, ShieldCheck, TreePine } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "@/registry/paragon/ui/button";

export interface OffsetMetric {
  label: string;
  value: string;
  icon: React.ElementType;
}

export interface OffsetCardProps extends React.ComponentProps<"div"> {
  project?: string;
  location?: string;
  type?: string;
  /** Registry/standard badge, e.g. "Verra VCS". */
  standard?: string;
  /** Price per tonne CO₂e. */
  price?: number;
  currency?: string;
  /** Tonnes available / funded fraction, 0–1. */
  funded?: number;
  metrics?: OffsetMetric[];
  onBuy?: () => void;
}

const DEFAULT_METRICS: OffsetMetric[] = [
  { label: "CO₂e removed", value: "12,400 t/yr", icon: Leaf },
  { label: "Area protected", value: "8,900 ha", icon: TreePine },
];

/**
 * A carbon-offset project card: a headed panel with a verification badge,
 * impact metrics, a funded-progress bar (scaleX, no layout thrash), and a buy
 * action that shows per-tonne price. Uses the Button primitive for the CTA and
 * tabular numerals throughout.
 */
export function OffsetCard({
  project = "Rimba Raya Biodiversity Reserve",
  location = "Central Kalimantan, Indonesia",
  type = "Forest conservation (REDD+)",
  standard = "Verra VCS",
  price = 18,
  currency = "$",
  funded = 0.72,
  metrics = DEFAULT_METRICS,
  onBuy,
  className,
  ...props
}: OffsetCardProps) {
  const pct = Math.round(Math.min(Math.max(funded, 0), 1) * 100);

  return (
    <div
      data-slot="offset-card"
      className={cn(
        "w-full max-w-sm overflow-hidden rounded-xl bg-card text-card-foreground shadow-border",
        className,
      )}
      {...props}
    >
      <div className="flex items-start gap-3 p-5">
        <span className="flex size-10 shrink-0 items-center justify-center rounded-lg bg-success/10 text-success">
          <Leaf className="size-5" />
        </span>
        <div className="min-w-0 flex-1">
          <h3 className="text-sm font-semibold leading-tight text-balance">
            {project}
          </h3>
          <p className="mt-1 flex items-center gap-1 text-xs text-muted-foreground">
            <MapPin className="size-3 shrink-0" />
            <span className="truncate">{location}</span>
          </p>
        </div>
      </div>

      <div className="flex flex-wrap items-center gap-2 px-5">
        <span className="rounded-full bg-secondary px-2.5 py-0.5 text-xs text-muted-foreground">
          {type}
        </span>
        <span className="flex items-center gap-1 rounded-full bg-success/10 px-2.5 py-0.5 text-xs font-medium text-success">
          <ShieldCheck className="size-3" />
          {standard}
        </span>
      </div>

      <div className="mt-4 grid grid-cols-2 gap-2 px-5">
        {metrics.map((m) => (
          <div key={m.label} className="rounded-lg bg-secondary/50 p-3">
            <m.icon className="size-4 text-muted-foreground" />
            <div className="mt-1.5 text-sm font-semibold tabular-nums">
              {m.value}
            </div>
            <div className="text-xs text-muted-foreground">{m.label}</div>
          </div>
        ))}
      </div>

      <div className="mt-4 px-5">
        <div className="flex items-center justify-between text-xs">
          <span className="text-muted-foreground">Funded</span>
          <span className="font-medium tabular-nums">{pct}%</span>
        </div>
        <div className="mt-1.5 h-1.5 overflow-hidden rounded-full bg-secondary">
          <div
            className="h-full origin-left rounded-full bg-success transition-transform duration-500 ease-out"
            style={{ transform: `scaleX(${pct / 100})` }}
          />
        </div>
      </div>

      <div className="mt-4 flex items-center justify-between border-t border-border p-5">
        <div>
          <div className="text-lg font-semibold tabular-nums">
            {currency}
            {price.toFixed(2)}
          </div>
          <div className="text-xs text-muted-foreground">per tonne CO₂e</div>
        </div>
        <Button onClick={onBuy}>Buy offsets</Button>
      </div>
    </div>
  );
}