Referral cards that advance through a four-node stage rail from requested to seen, with urgent tinting and a spring-eased fill as each referral steps forward.
npx shadcn@latest add @paragon/referral-tracker"use client";
import * as React from "react";
import { useReducedMotion } from "motion/react";
import { ArrowRight, Stethoscope } from "lucide-react";
import { cn } from "@/lib/utils";
const STAGES = ["Requested", "Authorized", "Scheduled", "Seen"] as const;
export type ReferralStage = (typeof STAGES)[number];
export interface Referral {
id: string;
patient: string;
specialty: string;
toProvider: string;
requested: string;
stage: ReferralStage;
/** Optional urgency tag. */
priority?: "routine" | "urgent";
}
export interface ReferralTrackerProps extends React.ComponentProps<"div"> {
referrals?: Referral[];
static?: boolean;
}
const DEFAULT_REFERRALS: Referral[] = [
{ id: "1", patient: "Dana Whitfield", specialty: "Cardiology", toProvider: "Dr. Okafor", requested: "Jun 10", stage: "Scheduled", priority: "routine" },
{ id: "2", patient: "Marcus Delgado", specialty: "Nephrology", toProvider: "Dr. Vásquez", requested: "Jun 12", stage: "Authorized", priority: "urgent" },
{ id: "3", patient: "Jordan Ellis", specialty: "Dermatology", toProvider: "Dr. Rahman", requested: "Jun 14", stage: "Requested" },
{ id: "4", patient: "Camila Ortega", specialty: "Orthopedics", toProvider: "Dr. Ortiz", requested: "Jun 08", stage: "Seen", priority: "routine" },
];
/**
* A referral tracker: each card advances through a four-node stage rail
* (requested → authorized → scheduled → seen). Advancing steps the active
* node forward with a spring-eased fill; urgent referrals carry a tint.
* Reduced motion drops the node transition to an instant color change.
*/
export function ReferralTracker({
referrals: initial = DEFAULT_REFERRALS,
static: isStatic = false,
className,
...props
}: ReferralTrackerProps) {
const reduced = useReducedMotion();
const [referrals, setReferrals] = React.useState(initial);
const animate = !isStatic && !reduced;
const advance = (id: string) =>
setReferrals((prev) =>
prev.map((r) => {
if (r.id !== id) return r;
const idx = STAGES.indexOf(r.stage);
return { ...r, stage: STAGES[Math.min(STAGES.length - 1, idx + 1)] };
}),
);
return (
<div
data-slot="referral-tracker"
className={cn("flex w-full max-w-md flex-col gap-2.5", className)}
{...props}
>
{referrals.map((r) => {
const idx = STAGES.indexOf(r.stage);
const complete = r.stage === "Seen";
return (
<article
key={r.id}
className="rounded-xl bg-card p-3.5 text-card-foreground shadow-border"
>
<div className="flex items-start gap-2.5">
<span
aria-hidden
className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-secondary text-secondary-foreground"
>
<Stethoscope className="size-4" />
</span>
<div className="min-w-0 flex-1">
<div className="flex items-baseline gap-2">
<p className="truncate text-sm font-medium">{r.patient}</p>
{r.priority === "urgent" && (
<span className="shrink-0 rounded bg-destructive/12 px-1.5 py-0.5 text-[10px] font-medium text-destructive">
Urgent
</span>
)}
</div>
<p className="text-xs text-muted-foreground">
{r.specialty} → {r.toProvider} ·{" "}
<span className="tabular-nums">req {r.requested}</span>
</p>
</div>
{!complete && (
<button
type="button"
aria-label={`Advance ${r.patient}'s referral`}
onClick={() => advance(r.id)}
className="flex size-7 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors duration-150 hover:bg-accent hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring outline-none"
>
<ArrowRight className="size-4" />
</button>
)}
</div>
<div className="mt-3 flex items-center gap-1.5">
{STAGES.map((s, i) => {
const reached = i <= idx;
return (
<React.Fragment key={s}>
<span className="flex flex-col items-center gap-1">
<span
className={cn(
"size-2.5 rounded-full",
reached
? complete && i === idx
? "bg-success"
: "bg-primary"
: "bg-secondary",
)}
style={{
transition: animate
? "background-color 300ms var(--ease-out)"
: undefined,
}}
/>
</span>
{i < STAGES.length - 1 && (
<span className="h-0.5 flex-1 overflow-hidden rounded-full bg-secondary">
<span
className="block h-full w-full origin-left rounded-full bg-primary"
style={{
transform: `scaleX(${i < idx ? 1 : 0})`,
transition: animate
? "transform 350ms var(--ease-out)"
: undefined,
}}
/>
</span>
)}
</React.Fragment>
);
})}
</div>
<div className="mt-1.5 flex justify-between text-[10px] text-muted-foreground">
{STAGES.map((s) => (
<span key={s} className="flex-1 text-center first:text-left last:text-right">
{s}
</span>
))}
</div>
</article>
);
})}
</div>
);
}