Logistics

Proof of Delivery

A delivery confirmation card with recipient, timestamp, a signature that traces in on mount, and a drop-photo frame.

Install

npx shadcn@latest add @paragon/proof-of-delivery

Also installs: tooltip

proof-of-delivery.tsx

"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>
  );
}