An e-prescribe form with type-ahead drug search, strength/quantity/refills selectors, pharmacy routing, and a sign-and-send confirmation.
npx shadcn@latest add @paragon/prescription-pad"use client";
import * as React from "react";
import { Pill, Search, Check } from "lucide-react";
import { cn } from "@/lib/utils";
export interface DrugOption {
name: string;
strengths: string[];
form: string;
}
export interface PrescriptionPadProps
extends Omit<React.ComponentProps<"form">, "onSubmit"> {
drugs?: DrugOption[];
pharmacies?: string[];
onSign?: (rx: {
drug: string;
dose: string;
quantity: number;
refills: number;
pharmacy: string;
}) => void;
}
const DEFAULT_DRUGS: DrugOption[] = [
{ name: "Amoxicillin", strengths: ["250 mg", "500 mg", "875 mg"], form: "Capsule" },
{ name: "Lisinopril", strengths: ["5 mg", "10 mg", "20 mg"], form: "Tablet" },
{ name: "Metformin", strengths: ["500 mg", "850 mg", "1000 mg"], form: "Tablet" },
{ name: "Atorvastatin", strengths: ["10 mg", "20 mg", "40 mg", "80 mg"], form: "Tablet" },
{ name: "Sertraline", strengths: ["25 mg", "50 mg", "100 mg"], form: "Tablet" },
];
const DEFAULT_PHARMACIES = [
"Cedar Grove Pharmacy — Main St",
"Riverside Health Rx — 4th Ave",
"Mail order — 90-day supply",
];
const fieldClass =
"h-9 w-full rounded-lg border border-input bg-card px-3 text-sm text-card-foreground transition-[border-color,box-shadow] duration-150 ease-(--ease-out) focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/40 outline-none";
/**
* An e-prescribe pad: type-ahead drug search filters a listbox, selecting a
* drug populates the strength/form and enables dose, quantity, refills and
* pharmacy. Signing summarizes the order. All state changes are instant or
* short color transitions — a clinical form, no decorative motion.
*/
export function PrescriptionPad({
drugs = DEFAULT_DRUGS,
pharmacies = DEFAULT_PHARMACIES,
onSign,
className,
...props
}: PrescriptionPadProps) {
const [query, setQuery] = React.useState("");
const [drug, setDrug] = React.useState<DrugOption | null>(null);
const [dose, setDose] = React.useState("");
const [quantity, setQuantity] = React.useState(30);
const [refills, setRefills] = React.useState(0);
const [pharmacy, setPharmacy] = React.useState(pharmacies[0]);
const [signed, setSigned] = React.useState(false);
const listId = React.useId();
const matches =
query.length > 0 && !drug
? drugs.filter((d) => d.name.toLowerCase().includes(query.toLowerCase()))
: [];
const ready = drug && dose;
return (
<form
data-slot="prescription-pad"
onSubmit={(e) => {
e.preventDefault();
if (!ready) return;
setSigned(true);
onSign?.({ drug: `${drug.name} ${dose}`, dose, quantity, refills, pharmacy });
}}
className={cn(
"w-full max-w-sm rounded-xl bg-card p-4 text-card-foreground shadow-border",
className,
)}
{...props}
>
<div className="mb-3 flex items-center gap-2">
<Pill aria-hidden className="size-4 text-muted-foreground" />
<h3 className="text-sm font-semibold">New prescription</h3>
</div>
<div className="relative">
<label className="text-xs font-medium text-muted-foreground" htmlFor={`${listId}-drug`}>
Medication
</label>
<div className="relative mt-1">
<Search
aria-hidden
className="pointer-events-none absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2 text-muted-foreground"
/>
<input
id={`${listId}-drug`}
role="combobox"
aria-expanded={matches.length > 0}
aria-controls={listId}
autoComplete="off"
placeholder="Search drug name…"
value={drug ? `${drug.name} · ${drug.form}` : query}
onChange={(e) => {
setQuery(e.target.value);
setDrug(null);
setDose("");
setSigned(false);
}}
className={cn(fieldClass, "pl-8")}
/>
</div>
{matches.length > 0 && (
<ul
id={listId}
role="listbox"
className="absolute z-10 mt-1 w-full overflow-hidden rounded-lg bg-popover py-1 shadow-overlay"
>
{matches.map((d) => (
<li key={d.name} role="option" aria-selected={false}>
<button
type="button"
onClick={() => {
setDrug(d);
setDose(d.strengths[0]);
setQuery("");
}}
className="flex w-full items-center justify-between px-3 py-1.5 text-left text-sm transition-colors duration-150 hover:bg-accent"
>
<span>{d.name}</span>
<span className="text-xs text-muted-foreground">{d.form}</span>
</button>
</li>
))}
</ul>
)}
</div>
<div className="mt-3 grid grid-cols-2 gap-3">
<label className="flex flex-col gap-1">
<span className="text-xs font-medium text-muted-foreground">Dose</span>
<select
disabled={!drug}
value={dose}
onChange={(e) => {
setDose(e.target.value);
setSigned(false);
}}
className={cn(fieldClass, "disabled:opacity-50")}
>
{drug ? (
drug.strengths.map((s) => <option key={s}>{s}</option>)
) : (
<option>—</option>
)}
</select>
</label>
<label className="flex flex-col gap-1">
<span className="text-xs font-medium text-muted-foreground">Quantity</span>
<input
type="number"
min={1}
inputMode="numeric"
value={quantity}
onChange={(e) => {
setQuantity(Math.max(1, Number(e.target.value) || 1));
setSigned(false);
}}
className={cn(fieldClass, "tabular-nums")}
/>
</label>
</div>
<div className="mt-3 grid grid-cols-2 gap-3">
<label className="flex flex-col gap-1">
<span className="text-xs font-medium text-muted-foreground">Refills</span>
<select
value={refills}
onChange={(e) => {
setRefills(Number(e.target.value));
setSigned(false);
}}
className={cn(fieldClass, "tabular-nums")}
>
{[0, 1, 2, 3, 5, 11].map((r) => (
<option key={r} value={r}>
{r}
</option>
))}
</select>
</label>
<label className="flex flex-col gap-1">
<span className="text-xs font-medium text-muted-foreground">Pharmacy</span>
<select
value={pharmacy}
onChange={(e) => {
setPharmacy(e.target.value);
setSigned(false);
}}
className={fieldClass}
>
{pharmacies.map((p) => (
<option key={p}>{p}</option>
))}
</select>
</label>
</div>
<button
type="submit"
disabled={!ready || signed}
className={cn(
"mt-4 inline-flex h-9 w-full items-center justify-center gap-2 rounded-lg text-sm font-medium transition-[background-color,color,scale] duration-150 ease-(--ease-out) focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-card outline-none active:not-disabled:scale-[0.98] disabled:opacity-50",
signed
? "bg-success text-success-foreground"
: "bg-primary text-primary-foreground",
)}
>
{signed ? (
<>
<Check className="size-4" /> Sent to pharmacy
</>
) : (
"Sign & send"
)}
</button>
</form>
);
}