An eligibility-check result card with plan identity, a coverage status pill, a copay/coinsurance grid, and deductible and out-of-pocket progress bars that fill on first view.
npx shadcn@latest add @paragon/insurance-eligibility"use client";
import * as React from "react";
import { useInView, useReducedMotion } from "motion/react";
import { ShieldCheck } from "lucide-react";
import { cn } from "@/lib/utils";
export interface InsuranceEligibilityProps extends React.ComponentProps<"div"> {
plan?: string;
memberId?: string;
status?: "active" | "inactive" | "pending";
copay?: number;
coinsurance?: number;
/** Amount of the deductible already met, in dollars. */
deductibleMet?: number;
/** Total deductible for the plan year, in dollars. */
deductibleTotal?: number;
/** Out-of-pocket amount met, in dollars. */
oopMet?: number;
oopMax?: number;
static?: boolean;
}
const usd = (n: number) =>
n.toLocaleString("en-US", { style: "currency", currency: "USD", maximumFractionDigits: 0 });
const statusMap: Record<
NonNullable<InsuranceEligibilityProps["status"]>,
{ label: string; dot: string; text: string }
> = {
active: { label: "Active", dot: "bg-success", text: "text-success" },
pending: { label: "Pending", dot: "bg-warning", text: "text-warning" },
inactive: { label: "Inactive", dot: "bg-destructive", text: "text-destructive" },
};
function Progress({
met,
total,
animate,
label,
}: {
met: number;
total: number;
animate: boolean;
label: string;
}) {
const pct = total > 0 ? Math.min(100, (met / total) * 100) : 0;
return (
<div>
<div className="flex items-baseline justify-between text-xs">
<span className="text-muted-foreground">{label}</span>
<span className="font-medium tabular-nums">
{usd(met)} <span className="text-muted-foreground">/ {usd(total)}</span>
</span>
</div>
<div className="mt-1.5 h-1.5 overflow-hidden rounded-full bg-secondary">
<div
className="h-full w-full origin-left rounded-full bg-primary"
style={{
transform: `scaleX(${pct / 100})`,
transition: animate ? "transform 700ms var(--ease-out)" : undefined,
}}
/>
</div>
</div>
);
}
/**
* An eligibility-check result: plan and member identity, a status pill, a
* cost-share grid (copay / coinsurance), and deductible + out-of-pocket
* progress bars that grow on first view. Reduced motion renders the bars at
* their final width immediately.
*/
export function InsuranceEligibility({
plan = "Blue Shield PPO — Gold 80",
memberId = "XZP 884 210 013",
status = "active",
copay = 25,
coinsurance = 20,
deductibleMet = 640,
deductibleTotal = 1500,
oopMet = 1820,
oopMax = 6000,
static: isStatic = false,
className,
...props
}: InsuranceEligibilityProps) {
const ref = React.useRef<HTMLDivElement>(null);
const reduced = useReducedMotion();
const inView = useInView(ref, { once: true, margin: "0px 0px -32px 0px" });
const s = statusMap[status];
const [grow, setGrow] = React.useState(false);
React.useEffect(() => {
if (isStatic || reduced) {
setGrow(true);
return;
}
if (inView) {
const id = requestAnimationFrame(() => setGrow(true));
return () => cancelAnimationFrame(id);
}
}, [inView, isStatic, reduced]);
const animate = !isStatic && !reduced;
return (
<div
ref={ref}
data-slot="insurance-eligibility"
className={cn(
"w-full max-w-sm rounded-xl bg-card p-4 text-card-foreground shadow-border",
className,
)}
{...props}
>
<div className="flex items-start gap-2.5">
<span
aria-hidden
className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-secondary text-secondary-foreground"
>
<ShieldCheck className="size-4" />
</span>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium">{plan}</p>
<p className="truncate text-xs text-muted-foreground tabular-nums">
Member {memberId}
</p>
</div>
<span className="inline-flex shrink-0 items-center gap-1.5 rounded-full bg-secondary px-2 py-0.5 text-xs font-medium">
<span aria-hidden className={cn("size-1.5 rounded-full", s.dot)} />
<span className={s.text}>{s.label}</span>
</span>
</div>
<div className="mt-4 grid grid-cols-2 gap-2">
<div className="rounded-lg bg-secondary/60 px-3 py-2.5">
<p className="text-lg font-semibold tabular-nums">{usd(copay)}</p>
<p className="text-[11px] text-muted-foreground">Office visit copay</p>
</div>
<div className="rounded-lg bg-secondary/60 px-3 py-2.5">
<p className="text-lg font-semibold tabular-nums">{coinsurance}%</p>
<p className="text-[11px] text-muted-foreground">Coinsurance</p>
</div>
</div>
<div className="mt-4 flex flex-col gap-3.5">
<Progress
label="Deductible met"
met={grow ? deductibleMet : 0}
total={deductibleTotal}
animate={animate}
/>
<Progress
label="Out-of-pocket met"
met={grow ? oopMet : 0}
total={oopMax}
animate={animate}
/>
</div>
</div>
);
}