AI

Content Credentials

A C2PA-flavoured provenance panel — a four-rung signature verdict, the signing issuer, the created/edited/exported chain, an expandable raw manifest, and a compact chip that opens the whole thing in a popover.

Preview in your theme

Loading preview…

"use client"

import * as React from "react"
import {
  Braces,
  Camera,
  Check,
  ChevronDown,
  Copy,
  FileUp,
  Layers,
  LoaderCircle,
  PenLine,
  Send,

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/content-credentials.json

Prompt

The prompt behind this component — paste it into your AI assistant to recreate or adapt it.

Build a React + TypeScript + Tailwind "ContentCredentials" component: a content
provenance panel in the spirit of C2PA / Content Credentials. It renders a
signature verdict, who signed the manifest, the production chain that led to
this file, and the raw manifest behind a disclosure. Ships with a compact chip
that opens the same panel in a popover. Uses lucide-react for glyphs, the
Radix Popover primitive for the chip, and a shadcn Button for icon actions.

Contract
- forwardRef<HTMLDivElement>, extends React.HTMLAttributes<HTMLDivElement>
  minus `title` (redefined as ReactNode). Rest props spread on the root.
- status?: "verified" | "checking" | "unverified" | "tampered" (default
  "verified"). This is the whole state machine — there is no separate `loading`
  or `error` flag.
- title?: ReactNode — overrides the verdict headline.
- description?: ReactNode — overrides the verdict sentence; `null` drops it.
- issuer?: { name: string; authority?: string; signedAt?: Date | string;
  fingerprint?: string } — omit the whole object when there is no signature to
  describe.
- chain?: readonly Step[] where Step = { id?, action?, actor, detail?, at?,
  ai?, broken? }. `action` ∈ "captured" | "created" | "imported" | "edited" |
  "exported" | "published" (default "edited") and picks the verb, the glyph and
  the accent token. `[]` renders a "no production history" note; omitting the
  prop drops the section entirely.
- manifest?: string | Record<string, unknown> — an object is pretty-printed
  with JSON.stringify(value, null, 2); a string is shown byte for byte (use it
  when the server already serialized it).
- manifestLabel?: string (default "Raw manifest"),
  defaultManifestOpen?: boolean (default false),
  onManifestOpenChange?: (open: boolean) => void — the hook for lazy fetching.
- formatDate?: (value: Date) => string — defaults to absolute UTC.
- footer?: ReactNode — policy link / "inspect elsewhere" action. The consumer
  owns its onClick or href; render nothing clickable that does nothing.
- Second export: <ContentCredentialsBadge> — same data props plus label?,
  side?, align?, panelClassName?; extends button props (className styles the
  chip, panelClassName styles the panel inside the popover), forwardRef to the
  button.

Behavior — the verdict ladder
- Four rungs, not a boolean, and "unverified" is the one that matters most:
  a file with NO signature is not a forged file. Give it its own neutral
  treatment (muted accent, dashed panel border, wording that says "unverified
  is not the same as forged") so it can never be mistaken for the alarm state.
- Each verdict also fixes how much the chain below may claim:
  verified → proof "signed"; checking and unverified → "claimed";
  tampered → "disputed". Nothing else in the component decides trust.
- tampered: find the first step flagged `broken`. That link and everything
  after it renders "disputed"; links ABOVE it are demoted to "claimed", never
  left looking signed — the signature that would have proved them is the one
  that just failed. With no step flagged, the entire chain is disputed.
- checking sets aria-busy on the root and spins the verdict glyph.
- One-shot settle: when `status` transitions to a settled rung, the verdict
  medallion plays a single scale/fade pop. Implement it as a CSS animation on a
  node keyed by a counter that only increments on a status change — a CSS
  animation starts on node insertion, so re-keying replays it exactly once and
  a parent re-render cannot retrigger it. No timer, nothing to clean up. Start
  the counter at 0 so the first paint (server and client) carries no animation
  class: a panel that mounts already verified must not pretend it just
  finished checking.

Behavior — the chain
- Vertical rail: a dot per step, a connector between consecutive dots. The
  connector is solid for "signed" and dashed for "claimed"/"disputed" —
  the line itself says whether the link is proven.
- Colour is earned. A signed step keeps its action accent (a color-mix tint of
  var(--chart-*)); a claimed step falls back to muted with a dashed dot ring;
  a disputed step goes destructive. Never colour a claim you cannot back.
- Each step reads: action verb (small caps), optional "AI" marker for
  model-produced output, optional "signature break" marker on the flagged
  link, then the actor in the strongest weight, then detail, then timestamp.
  Actor is the answer to "who did this", so it gets the emphasis, not the verb.

Behavior — timestamps
- Default format is absolute UTC ("2026-03-14 09:41 UTC"), derived from
  toISOString(). Two reasons: a provenance timestamp is a forensic value that
  must mean the same thing to the reader and to whoever they forward it to,
  and a locale-dependent format would differ between server and client and
  break hydration. Consumers who know they are client-only pass `formatDate`.
- A `string` timestamp passes through verbatim (already localized upstream);
  an unparseable Date is dropped, never printed as "Invalid Date".

Behavior — raw manifest disclosure
- A <button aria-expanded aria-controls> toggling a region that stays MOUNTED
  and uses the `hidden` attribute: aria-controls must point at a node that
  exists, and reopening must not lose the reader's scroll position.
- The row shows the label and a line count; the body is a <pre> capped at
  ~16rem with its own scroll, so a 400-line manifest cannot stretch the panel.
- A copy control sits over the top-right of the body (with matching pre
  padding so text never hides under it). Same control next to the certificate
  serial: both values exist to be pasted into a ticket or a verifier.
- Copy control details: guard `navigator.clipboard` (absent on insecure
  origins and in some webviews) and stay idle instead of throwing; swap the
  icon for ~1.6s; clear the pending timeout before starting a new one so
  double-clicks do not flip the icon back early; clear it on unmount so a
  popover closing mid-timeout cannot set state on a dead node.

Behavior — the compact chip
- The chip is not a smaller panel; it is the affordance that admits a panel
  exists. It carries the verdict glyph so bad news is visible without a click.
- It opens the FULL panel in a popover, never a tooltip: the panel contains
  scrollable, selectable and copyable content a pointer must be able to reach.
- Portal the popover: the chip usually sits inside an image wrapper with
  overflow-hidden that would clip an in-place panel. Let Radix own
  aria-haspopup / aria-expanded / focus return.

Accessibility
- The verdict line is role="status" aria-live="polite": the flip from
  "Checking credentials…" to a result is the single most important thing a
  screen-reader user must hear, and it is the only live region carrying the
  panel's own content.
- The copy buttons hold the one other live region: an sr-only role="status"
  inside each button, mounted empty from the first paint and filled with
  "Copied to clipboard" once the write resolves. It has to exist before the
  message (a region created together with its first message is never
  announced), and the icon swap on its own is silent.
- Issuer and chain are role="group" with aria-labelledby pointing at their
  section labels (useId-derived), so the panel does not inject headings into
  the host document's outline.
- The chain is an <ol> — order is meaning here.
- Every glyph is decorative and aria-hidden; the "AI" marker carries an
  sr-only expansion ("produced by a model"); focus-visible rings on the
  disclosure button, the copy buttons and the chip.

Rendering & styling
- Semantic tokens only: bg-card / text-card-foreground for the surface, border,
  bg-muted/40 for the issuer block, text-muted-foreground for secondary text,
  text-destructive + border-destructive/40 + bg-destructive/10 for the disputed
  treatment, ring / ring-ring for focus, and var(--chart-1..5) plus
  var(--destructive) / var(--muted-foreground) as accents. Every tint is
  color-mix(in oklab, <token> N%, transparent) — no hex, no rgb(), no oklch().
- Merge consumer className with cn() so `w-[22rem]` in the popover can
  override the panel's own `w-full`.
- Reduced motion: the settle pop is motion-reduce:[animation:none], the
  checking spinner motion-reduce:animate-none, the chevron rotation and the
  popover enter/exit motion-reduce:*-none. Every verdict stays legible without
  a single frame of animation.
- Long values (issuer names, actors, certificate serials) use wrap-anywhere and
  min-w-0 so nothing widens the panel; the certificate serial is shown IN FULL
  rather than middle-truncated — a serial you cannot read is a serial you
  cannot compare.

Customization levers
- Verdict table: one Record<status, {title, description, icon, accent, proof,
  history}> at the top of the file. Rewrite the copy, swap ShieldCheck /
  ShieldAlert for your icon set, or repoint `accent` at var(--primary) — the
  whole panel re-skins from that one table.
- Action vocabulary: the ACTIONS record is the closed verb list. Add
  "translated" / "watermarked" / "redacted" rows, or collapse to
  created/edited/published if your pipeline only ever asserts three.
- Sub-blocks are opt-in: pass only `status` for a one-line verdict card; add
  `issuer` for signer details, `chain` for the timeline, `manifest` for the
  evidence, `footer` for a policy link. Nothing renders a section it has no
  data for.
- Density: the root's gap-4 and each step's pb-3 / size-7 dot are the only
  spacing knobs; drop to gap-3 / pb-2 / size-6 for a sidebar, or raise for a
  dedicated verification page.
- Chain proof mapping: if your validator can prove links individually, change
  the disputed branch so links above the break stay "signed" instead of being
  demoted — one expression, and the rest of the rendering follows.
- Manifest body: swap the <pre> for a syntax highlighter or a key/value table,
  raise the max-height, or drop `manifest` entirely for consumer-facing pages
  where the JSON would be noise.
- Chip: change `label` to a single glyph plus sr-only text for an overlay on
  an image corner, or set side/align to open upward inside a card.

Concepts

  • Verdict ladder — four rungs instead of a checkmark, because "no signature found" and "the signature failed" are different facts; the unverified rung gets its own neutral wording and a dashed panel edge so it can never borrow the alarm treatment that belongs to a real mismatch.
  • Trust decays below the break — the verdict picks a proof mode for the whole chain, and a flagged break splits it again: links after the break go destructive, links before it drop to "claimed" rather than staying confident, since the signature that would have proved them is exactly the one that failed.
  • Colour is earned — accent tints are a reward for a signed link; unproven steps fall back to muted with a dashed rail and disputed steps to destructive, so the eye can read confidence off the timeline without reading a word.
  • Disclosure, not decoration — the summary above is an interpretation, so the raw manifest sits one click away behind aria-expanded, stays mounted while hidden (the control must point at a node that exists) and ships with a copy action, because a provenance UI that only ever shows its own summary is unfalsifiable.
  • One-shot settle — the verdict medallion pops exactly once per verdict: a CSS animation only starts when a node is inserted, so re-keying it on each status change is the whole lock — no timer, no replay on parent re-render, and no animation at all on first paint or under reduced motion.
  • Chip as trigger, not summary — the compact variant carries only the verdict glyph and a label, and opens the same panel in a portalled popover instead of a tooltip, because everything inside is scrollable, selectable and copyable and a pointer has to be able to reach it.

On This Page