{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"evidence-source","type":"registry:component","title":"Expandable Evidence Source","description":"A citation that unfolds from one line into an inspectable source card.","dependencies":[],"categories":["agentic","citations","progressive-disclosure","research","trust"],"files":[{"path":"components/lab/evidence-source/evidence-source.tsx","target":"components/lab/evidence-source/evidence-source.tsx","type":"registry:component","content":"\"use client\";\n\nimport { useId, useState } from \"react\";\nimport \"./evidence-source.css\";\n\nexport type EvidenceStatus = \"verified\" | \"probable\" | \"unverified\";\n\nexport type EvidenceSourceData = {\n  id: string;\n  title: string;\n  publisher?: string;\n  excerpt: string;\n  location?: string;\n  status?: EvidenceStatus;\n  confidence?: number;\n  date?: string;\n  href?: string;\n};\n\nexport type EvidenceSourceProps = {\n  source: EvidenceSourceData;\n  /** Start unfolded. */\n  defaultOpen?: boolean;\n  /** Controlled expansion. */\n  open?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  /** Overrides source.status when provided. */\n  status?: EvidenceStatus;\n  /** 0–1; renders as a small meter when provided. */\n  confidence?: number;\n  /** Fired by the “Open source” action so hosts can route it. */\n  onOpenSource?: (source: EvidenceSourceData) => void;\n  className?: string;\n};\n\nconst STATUS_LABEL: Record<EvidenceStatus, string> = {\n  verified: \"Verified\",\n  probable: \"Probable\",\n  unverified: \"Unverified\",\n};\n\nconst clamp01 = (value: number) => Math.min(1, Math.max(0, value));\n\n/**\n * EXPANDABLE EVIDENCE SOURCE — a citation that unfolds in place.\n *\n * Collapsed it is one compact line: glyph, title, status word, chevron.\n * Unfolded it grows into an inspectable card — excerpt, location, date,\n * publisher, a confidence reading and the way out to the original.\n * The expansion is a grid-template-rows transition, so it opens in\n * flow without measuring anything; reduced motion makes it instant.\n */\nexport function EvidenceSource({\n  source,\n  defaultOpen = false,\n  open,\n  onOpenChange,\n  status,\n  confidence,\n  onOpenSource,\n  className,\n}: EvidenceSourceProps) {\n  const [internalOpen, setInternalOpen] = useState(defaultOpen);\n  const panelId = useId();\n\n  const isControlled = open !== undefined;\n  const isOpen = isControlled ? open : internalOpen;\n  const effectiveStatus = status ?? source.status ?? \"verified\";\n  const effectiveConfidence = confidence ?? source.confidence;\n\n  const toggle = () => {\n    const next = !isOpen;\n    if (!isControlled) setInternalOpen(next);\n    onOpenChange?.(next);\n  };\n\n  const meta = [\n    source.location ? { label: \"Location\", value: source.location } : null,\n    source.date ? { label: \"Date\", value: source.date } : null,\n    source.publisher ? { label: \"Publisher\", value: source.publisher } : null,\n  ].filter((item): item is { label: string; value: string } => item !== null);\n\n  const confidencePercent =\n    typeof effectiveConfidence === \"number\"\n      ? Math.round(clamp01(effectiveConfidence) * 100)\n      : null;\n\n  return (\n    <div\n      className={`es-root${className ? ` ${className}` : \"\"}`}\n      data-open={isOpen ? \"true\" : \"false\"}\n    >\n      <button\n        type=\"button\"\n        className=\"es-summary\"\n        aria-expanded={isOpen}\n        aria-controls={panelId}\n        onClick={toggle}\n      >\n        <span className=\"es-glyph\" aria-hidden=\"true\">\n          <svg\n            viewBox=\"0 0 16 16\"\n            width=\"14\"\n            height=\"14\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            strokeWidth=\"1.3\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n          >\n            <path d=\"M4 1.75h5.25L12.5 5v9.25h-8.5z\" />\n            <path d=\"M9.25 1.75V5h3.25\" />\n          </svg>\n        </span>\n        <span className=\"es-title\">{source.title}</span>\n        <span className=\"es-sep\" aria-hidden=\"true\">\n          ·\n        </span>\n        <span className=\"es-status\" data-status={effectiveStatus}>\n          <span className=\"es-status-dot\" aria-hidden=\"true\" />\n          {STATUS_LABEL[effectiveStatus]}\n        </span>\n        <span className=\"es-chevron\" aria-hidden=\"true\">\n          <svg\n            viewBox=\"0 0 12 12\"\n            width=\"11\"\n            height=\"11\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            strokeWidth=\"1.5\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n          >\n            <path d=\"M2.5 4.5 6 8l3.5-3.5\" />\n          </svg>\n        </span>\n      </button>\n\n      <div\n        id={panelId}\n        className=\"es-panel\"\n        role=\"region\"\n        aria-label={`Source details: ${source.title}`}\n        inert={!isOpen}\n      >\n        <div className=\"es-panel-inner\">\n          <div className=\"es-body\">\n            <p className=\"es-excerpt\">{source.excerpt}</p>\n\n            {meta.length > 0 ? (\n              <dl className=\"es-meta\">\n                {meta.map((item) => (\n                  <div className=\"es-meta-row\" key={item.label}>\n                    <dt>{item.label}</dt>\n                    <dd>{item.value}</dd>\n                  </div>\n                ))}\n              </dl>\n            ) : null}\n\n            {confidencePercent !== null ? (\n              <div className=\"es-confidence\">\n                <span className=\"es-confidence-label\">Confidence</span>\n                <span className=\"es-meter\" aria-hidden=\"true\">\n                  <span\n                    className=\"es-meter-fill\"\n                    style={{ width: `${confidencePercent}%` }}\n                  />\n                </span>\n                <span className=\"es-confidence-value\">\n                  {confidencePercent}%\n                </span>\n              </div>\n            ) : null}\n\n            <div className=\"es-open-row\">\n              {source.href ? (\n                <a\n                  className=\"es-open\"\n                  href={source.href}\n                  target=\"_blank\"\n                  rel=\"noreferrer\"\n                  onClick={() => onOpenSource?.(source)}\n                >\n                  Open source <span aria-hidden=\"true\">↗</span>\n                  <span className=\"es-sr-only sr-only\">\n                    {\" \"}\n                    (opens in a new tab)\n                  </span>\n                </a>\n              ) : (\n                <button\n                  type=\"button\"\n                  className=\"es-open\"\n                  onClick={() => onOpenSource?.(source)}\n                >\n                  Open source <span aria-hidden=\"true\">↗</span>\n                </button>\n              )}\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"},{"path":"components/lab/evidence-source/evidence-source.css","target":"components/lab/evidence-source/evidence-source.css","type":"registry:file","content":"/* ============================================================\n   EXPANDABLE EVIDENCE SOURCE — self-contained styles.\n   Scoped tokens with fallbacks; the unfolding is a\n   grid-template-rows transition with a visibility hand-off so a\n   collapsed panel is never in the tab order or the a11y tree.\n   ============================================================ */\n\n.es-root {\n  --es-ink: #141418;\n  --es-ink-soft: #3a3a42;\n  --es-muted: #5f5f68;\n  --es-faint: #8c8c94;\n  --es-surface: #ffffff;\n  --es-surface-soft: #f8f8f6;\n  --es-line: rgba(20, 20, 24, 0.12);\n  --es-line-soft: rgba(20, 20, 24, 0.07);\n  --es-accent: #4f27e0;\n  --es-accent-deep: #32119c;\n  --es-ok: #147a4e;\n  --es-warn: #8a5300;\n  --es-grey: #8c8c94;\n  --es-serif: Georgia, \"Iowan Old Style\", \"Times New Roman\", serif;\n  --es-mono: ui-monospace, \"SF Mono\", \"Cascadia Mono\", Consolas, monospace;\n  --es-ease: cubic-bezier(0.22, 1, 0.36, 1);\n\n  min-width: 0;\n  border: 1px solid var(--es-line-soft);\n  border-radius: 12px;\n  background: var(--es-surface);\n  color: var(--es-ink);\n  font-size: 0.875rem;\n  line-height: 1.5;\n}\n\n.es-root :focus-visible {\n  outline: 2px solid var(--es-accent);\n  outline-offset: 2px;\n}\n\n/* Visually hidden, in case the host has no utility class of its own. */\n.es-sr-only {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  padding: 0;\n  margin: -1px;\n  overflow: hidden;\n  clip: rect(0 0 0 0);\n  white-space: nowrap;\n  border: 0;\n}\n\n/* ---- summary row ---- */\n\n.es-summary {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n  width: 100%;\n  padding: 10px 12px;\n  border: 0;\n  border-radius: 12px;\n  background: none;\n  font: inherit;\n  color: var(--es-ink);\n  text-align: left;\n  cursor: pointer;\n}\n\n.es-root[data-open=\"true\"] .es-summary {\n  border-radius: 12px 12px 0 0;\n}\n\n.es-summary:hover {\n  background: var(--es-surface-soft);\n}\n\n.es-summary:focus-visible {\n  outline-offset: -2px;\n}\n\n.es-glyph {\n  display: inline-flex;\n  flex: none;\n  color: var(--es-faint);\n}\n\n.es-title {\n  flex: 1 1 auto;\n  min-width: 0;\n  overflow: hidden;\n  font-size: 0.875rem;\n  font-weight: 500;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.es-sep {\n  flex: none;\n  color: var(--es-faint);\n}\n\n.es-status {\n  display: inline-flex;\n  align-items: center;\n  gap: 5px;\n  flex: none;\n  font-size: 0.75rem;\n  font-weight: 500;\n}\n\n.es-status[data-status=\"verified\"] {\n  color: var(--es-ok);\n}\n\n.es-status[data-status=\"probable\"] {\n  color: var(--es-warn);\n}\n\n.es-status[data-status=\"unverified\"] {\n  color: var(--es-grey);\n}\n\n.es-status-dot {\n  flex: none;\n  width: 6px;\n  height: 6px;\n  border-radius: 50%;\n  background: currentColor;\n}\n\n.es-chevron {\n  display: inline-flex;\n  flex: none;\n  color: var(--es-faint);\n  transition: transform 240ms var(--es-ease);\n}\n\n.es-root[data-open=\"true\"] .es-chevron {\n  transform: rotate(180deg);\n}\n\n/* ---- unfolding panel ---- */\n\n.es-panel {\n  display: grid;\n  grid-template-rows: 0fr;\n  opacity: 0;\n  visibility: hidden;\n  transition:\n    grid-template-rows 320ms var(--es-ease),\n    opacity 220ms ease,\n    visibility 0s linear 320ms;\n}\n\n.es-root[data-open=\"true\"] .es-panel {\n  grid-template-rows: 1fr;\n  opacity: 1;\n  visibility: visible;\n  transition:\n    grid-template-rows 320ms var(--es-ease),\n    opacity 220ms ease 40ms,\n    visibility 0s;\n}\n\n.es-panel-inner {\n  min-height: 0;\n  overflow: hidden;\n}\n\n.es-body {\n  padding: 12px;\n  border-top: 1px solid var(--es-line-soft);\n}\n\n/* ---- excerpt (system serif, roomier leading — no new fonts) ---- */\n\n.es-excerpt {\n  margin: 0;\n  font-family: var(--es-serif);\n  font-size: 0.9375rem;\n  font-style: normal;\n  line-height: 1.75;\n  color: var(--es-ink-soft);\n  overflow-wrap: anywhere;\n}\n\n/* ---- meta grid ---- */\n\n.es-meta {\n  display: grid;\n  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));\n  gap: 8px 16px;\n  margin: 12px 0 0;\n}\n\n.es-meta-row {\n  min-width: 0;\n}\n\n.es-meta dt {\n  font-family: var(--es-mono);\n  font-size: 0.5625rem;\n  letter-spacing: 0.1em;\n  text-transform: uppercase;\n  color: var(--es-faint);\n}\n\n.es-meta dd {\n  margin: 2px 0 0;\n  font-size: 0.8125rem;\n  color: var(--es-ink-soft);\n  overflow-wrap: anywhere;\n}\n\n/* ---- confidence ---- */\n\n.es-confidence {\n  display: flex;\n  align-items: center;\n  gap: 10px;\n  margin-top: 12px;\n}\n\n.es-confidence-label {\n  flex: none;\n  font-size: 0.75rem;\n  color: var(--es-muted);\n}\n\n.es-meter {\n  flex: 1 1 auto;\n  min-width: 48px;\n  height: 4px;\n  border-radius: 999px;\n  background: rgba(20, 20, 24, 0.08);\n  overflow: hidden;\n}\n\n.es-meter-fill {\n  display: block;\n  height: 100%;\n  border-radius: 999px;\n  background: var(--es-ink-soft);\n}\n\n.es-confidence-value {\n  flex: none;\n  font-family: var(--es-mono);\n  font-size: 0.6875rem;\n  color: var(--es-muted);\n}\n\n/* ---- open action ---- */\n\n.es-open-row {\n  margin-top: 12px;\n}\n\n.es-open {\n  display: inline-flex;\n  align-items: center;\n  gap: 6px;\n  padding: 3px 0;\n  border: 0;\n  background: none;\n  font: inherit;\n  font-size: 0.8125rem;\n  font-weight: 500;\n  color: var(--es-accent-deep);\n  text-decoration: underline;\n  text-decoration-color: rgba(50, 17, 156, 0.35);\n  text-underline-offset: 3px;\n  cursor: pointer;\n}\n\n.es-open:hover {\n  text-decoration-color: currentColor;\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .es-panel,\n  .es-root[data-open=\"true\"] .es-panel,\n  .es-chevron {\n    transition: none;\n  }\n}\n"}]}