A delivery confirmation card with recipient, timestamp, a signature that traces in on mount, and a drop-photo frame.
npx shadcn@latest add @paragon/proof-of-deliveryAlso installs: tooltip
"use client";
import * as React from "react";
import { useInView, useReducedMotion } from "motion/react";
import { Check, Camera, MapPin, Clock, User } from "lucide-react";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/registry/paragon/ui/tooltip";
import { cn } from "@/lib/utils";
export interface ProofOfDeliveryProps extends React.ComponentProps<"div"> {
/** Order / tracking reference. */
reference: string;
recipient: string;
/** Formatted delivery timestamp, e.g. "Jul 6, 2026 · 2:41 PM". */
timestamp: string;
/** Drop location note. */
location?: string;
/** SVG path data for the captured signature stroke (in a 0 0 200 80 box). */
signaturePath?: string;
/** Whether a delivery photo was captured (shows placeholder frame). */
hasPhoto?: boolean;
/** Opt out of the reveal + signature trace. */
static?: boolean;
}
const DEFAULT_SIGNATURE =
"M6 52 C 20 20, 34 20, 40 44 S 58 68, 70 40 88 18, 104 46 122 66, 140 40 158 22, 194 34";
/**
* A proof-of-delivery card: a confirmed badge, recipient, timestamp and drop
* location, a captured signature drawn as an SVG stroke that traces in on first
* view (stroke-dashoffset), and a delivery-photo frame. The card reveals with
* the house rise (opacity + translateY + blur) and staggers its rows in; the
* meta icons carry hover/focus tooltips. Deterministic — the signature path and
* timestamp are props. Reduced motion (or `static`) shows everything settled.
*/
export function ProofOfDelivery({
reference,
recipient,
timestamp,
location,
signaturePath = DEFAULT_SIGNATURE,
hasPhoto = true,
static: isStatic = false,
className,
...props
}: ProofOfDeliveryProps) {
const ref = React.useRef<HTMLDivElement>(null);
const reducedMotion = useReducedMotion();
const inView = useInView(ref, { once: true, margin: "0px 0px -24px 0px" });
const animate = !isStatic && !reducedMotion;
const armed = !animate || inView;
const rise = (delay: number): React.CSSProperties | undefined =>
animate
? {
animation: `pod-rise var(--duration-base) var(--ease-out) both`,
animationDelay: `${delay}ms`,
}
: undefined;
return (
<TooltipProvider>
<style href="paragon-proof-of-delivery" precedence="paragon">{`
@keyframes pod-rise {
from { opacity: 0; transform: translateY(12px); filter: blur(4px); }
to { opacity: 1; transform: translateY(0); filter: blur(0); }
}
@keyframes pod-sign {
from { stroke-dashoffset: 100; }
to { stroke-dashoffset: 0; }
}
@media (prefers-reduced-motion: reduce) {
[data-pod-sign], [data-pod-rise] { animation: none !important; }
[data-pod-sign] { stroke-dashoffset: 0 !important; }
}
`}</style>
<div
ref={ref}
data-slot="proof-of-delivery"
data-pod-rise
className={cn(
"w-full max-w-sm rounded-xl bg-card p-5 text-card-foreground shadow-border",
className,
)}
style={rise(0)}
{...props}
>
<div className="flex items-center gap-2" style={rise(40)}>
<span className="flex size-6 shrink-0 items-center justify-center rounded-full bg-success text-success-foreground">
<Check aria-hidden className="size-3.5" strokeWidth={3} />
</span>
<p className="text-sm font-medium">Delivered</p>
<Tooltip>
<TooltipTrigger asChild>
<span
tabIndex={0}
className="ml-auto cursor-default truncate rounded font-mono text-xs tabular-nums text-muted-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
>
{reference}
</span>
</TooltipTrigger>
<TooltipContent side="top">Tracking reference</TooltipContent>
</Tooltip>
</div>
<dl className="mt-4 grid grid-cols-1 gap-2 text-sm" style={rise(80)}>
<div className="flex items-center justify-between gap-3">
<dt className="flex shrink-0 items-center gap-1.5 text-muted-foreground">
<Tooltip>
<TooltipTrigger asChild>
<User
aria-hidden
className="size-3.5 shrink-0 text-muted-foreground"
/>
</TooltipTrigger>
<TooltipContent side="top">Signed by</TooltipContent>
</Tooltip>
Recipient
</dt>
<dd className="min-w-0 truncate text-right font-medium">
{recipient}
</dd>
</div>
<div className="flex items-center justify-between gap-3">
<dt className="flex shrink-0 items-center gap-1.5 text-muted-foreground">
<Tooltip>
<TooltipTrigger asChild>
<Clock
aria-hidden
className="size-3.5 shrink-0 text-muted-foreground"
/>
</TooltipTrigger>
<TooltipContent side="top">Delivery time</TooltipContent>
</Tooltip>
Time
</dt>
<dd className="min-w-0 truncate text-right tabular-nums">
{timestamp}
</dd>
</div>
{location && (
<div className="flex items-center justify-between gap-3">
<dt className="flex shrink-0 items-center gap-1.5 text-muted-foreground">
<Tooltip>
<TooltipTrigger asChild>
<MapPin
aria-hidden
className="size-3.5 shrink-0 text-muted-foreground"
/>
</TooltipTrigger>
<TooltipContent side="top">Drop location</TooltipContent>
</Tooltip>
Location
</dt>
<dd className="min-w-0 truncate text-right">{location}</dd>
</div>
)}
</dl>
<div className="mt-4 grid grid-cols-2 gap-3" style={rise(120)}>
<figure className="rounded-lg border border-border bg-secondary/40 p-2">
<svg
viewBox="0 0 200 80"
className="h-16 w-full"
role="img"
aria-label="Recipient signature"
preserveAspectRatio="xMidYMid meet"
>
<path
data-pod-sign
d={signaturePath}
fill="none"
className="stroke-foreground"
strokeWidth={2.5}
strokeLinecap="round"
strokeLinejoin="round"
pathLength={100}
vectorEffect="non-scaling-stroke"
style={{
strokeDasharray: 100,
strokeDashoffset: armed ? 0 : 100,
animation: armed
? "pod-sign 1.2s var(--ease-out) forwards"
: undefined,
}}
/>
</svg>
<figcaption className="mt-1 truncate text-center text-[11px] text-muted-foreground">
Signature
</figcaption>
</figure>
<figure className="flex flex-col rounded-lg border border-border bg-secondary/40 p-2">
<div className="flex flex-1 items-center justify-center rounded-md bg-secondary text-muted-foreground">
{hasPhoto ? (
<Tooltip>
<TooltipTrigger asChild>
<Camera
aria-label="Delivery photo captured"
tabIndex={0}
className="size-6 rounded outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
/>
</TooltipTrigger>
<TooltipContent side="top">
Photo captured at drop-off
</TooltipContent>
</Tooltip>
) : (
<span className="text-[11px]">No photo</span>
)}
</div>
<figcaption className="mt-1 truncate text-center text-[11px] text-muted-foreground">
Drop photo
</figcaption>
</figure>
</div>
</div>
</TooltipProvider>
);
}