A carbon-offset project card with a verification badge, impact metrics, a funded-progress bar, and a per-tonne buy action.
npx shadcn@latest add @paragon/offset-cardAlso installs: button
"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>
);
}