A completion certificate that eases into view with a single restrained shine sweep.
npx shadcn@latest add @paragon/certificate-reveal"use client";
import * as React from "react";
import { useInView, useReducedMotion } from "motion/react";
import { Award } from "lucide-react";
import { cn } from "@/lib/utils";
export interface CertificateRevealProps
extends React.ComponentProps<"div"> {
/** Recipient name. */
recipient: string;
/** Course or achievement title. */
title: string;
/** Issuing org. */
issuer?: string;
/** Completion date label. */
date?: string;
/** Credential id, shown small at the foot. */
credentialId?: string;
/** Suppresses the reveal and shine. */
static?: boolean;
}
/**
* A completion certificate that reveals once when it enters view: the card
* eases up and a single restrained shine sweeps across it (a masked gradient
* translated once, then done — never looping). Reduced motion shows the
* finished certificate with no reveal and no sweep.
*/
export function CertificateReveal({
recipient,
title,
issuer = "Paragon Academy",
date,
credentialId,
static: isStatic = false,
className,
...props
}: CertificateRevealProps) {
const ref = React.useRef<HTMLDivElement>(null);
const reducedMotion = useReducedMotion();
const inView = useInView(ref, { once: true, margin: "0px 0px -48px 0px" });
const animate = !isStatic && !reducedMotion;
const revealed = !animate || inView;
return (
<div
ref={ref}
data-slot="certificate-reveal"
className={cn("w-full max-w-md", className)}
{...props}
>
<style href="paragon-certificate-reveal" precedence="paragon">{`
/* A wide, soft specular band glides across once. The band is 150% of
the card wide and blurred, so its edges are feathered — it reads as
light catching foil, not a rectangle sliding by. It starts fully off
the left (-160%) and exits fully off the right (260%). Opacity eases
in then out over the pass so there is no hard on/off pop. */
@keyframes certificate-shine {
0% { transform: translateX(-160%) rotate(12deg); opacity: 0; }
18% { opacity: 1; }
82% { opacity: 1; }
100% { transform: translateX(260%) rotate(12deg); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
[data-certificate-shine] { animation: none !important; opacity: 0 !important; }
}
`}</style>
<div
className="relative overflow-hidden rounded-xl bg-card p-8 text-center shadow-border"
style={{
opacity: revealed ? 1 : 0,
transform: revealed ? "translateY(0)" : "translateY(12px)",
filter: revealed ? "blur(0px)" : "blur(4px)",
transition: animate
? "opacity 500ms var(--ease-out), transform 500ms var(--ease-out), filter 500ms var(--ease-out)"
: undefined,
}}
>
{/* Inner hairline frame, like a real certificate's double rule.
Concentric: 14px outer radius − 8px inset = 6px. */}
<div
aria-hidden
className="pointer-events-none absolute inset-2 rounded-[calc(var(--radius-xl)-8px)] border border-border/70"
/>
{/* Single specular sweep — a soft, wide, feathered sheen that glides
once after the reveal settles. Extra-tall/wide so the rotated,
blurred band never shows an edge; the gradient peak is low-opacity
with a bright hairline core, so it feels like light, not a box. */}
{animate && revealed && (
<span
data-certificate-shine
aria-hidden
className="pointer-events-none absolute -inset-y-8 left-0 w-[55%] blur-md"
style={{
background:
"linear-gradient(105deg, transparent 30%, color-mix(in oklch, var(--color-foreground) 6%, transparent) 44%, color-mix(in oklch, var(--color-foreground) 13%, transparent) 50%, color-mix(in oklch, var(--color-foreground) 6%, transparent) 56%, transparent 70%)",
animation: "certificate-shine 1200ms var(--ease-in-out) 400ms 1 both",
}}
/>
)}
<span
className="mx-auto flex size-12 items-center justify-center rounded-full bg-primary text-primary-foreground"
aria-hidden
>
<Award className="size-6" />
</span>
<p className="mt-4 text-xs font-medium tracking-[0.2em] text-muted-foreground uppercase">
Certificate of Completion
</p>
<p className="mt-4 text-xs text-muted-foreground">
This certifies that
</p>
<p className="mt-1 text-2xl font-semibold text-balance">{recipient}</p>
<p className="mt-3 text-xs text-muted-foreground">
has successfully completed
</p>
<p className="mt-1 text-base font-medium text-balance">{title}</p>
<div className="mt-6 flex items-center justify-center gap-6 border-t border-border pt-4 text-xs text-muted-foreground">
<span>{issuer}</span>
{date && (
<>
<span aria-hidden className="text-border">
•
</span>
<span className="tabular-nums">{date}</span>
</>
)}
</div>
{credentialId && (
<p className="mt-2 text-[11px] text-muted-foreground/70 tabular-nums">
Credential {credentialId}
</p>
)}
</div>
</div>
);
}