{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"citation-ui","type":"registry:ui","title":"Citation UI","description":"Inline citation markers in generated text — numbered footnotes that reveal the source in a hover preview card.","author":"Scrim UI (https://scrimui.dev)","categories":["sources"],"docs":"https://scrimui.dev/components/citation-ui","dependencies":[],"registryDependencies":[],"files":[{"path":"src/showcase/citation-ui/citation-ui.tsx","content":"\"use client\";\n\nimport * as React from \"react\";\n\n/* ------------------------------------------------------------------ */\n/* Types                                                               */\n/* ------------------------------------------------------------------ */\n\nexport type Citation = {\n  id: number;\n  title: string;\n  url: string;\n  domain?: string;\n  snippet?: string;\n};\n\nexport type InlineCitationProps = {\n  citation: Citation;\n  className?: string;\n};\n\n/* ------------------------------------------------------------------ */\n/* Helpers                                                             */\n/* ------------------------------------------------------------------ */\n\nfunction domainFromUrl(url: string) {\n  try {\n    return new URL(url).hostname.replace(/^www\\./, \"\");\n  } catch {\n    return url;\n  }\n}\n\n/* ------------------------------------------------------------------ */\n/* InlineCitation                                                      */\n/* ------------------------------------------------------------------ */\n\nexport function InlineCitation({ citation, className = \"\" }: InlineCitationProps) {\n  const [open, setOpen] = React.useState(false);\n  const host = citation.domain ?? domainFromUrl(citation.url);\n\n  return (\n    <span className={`relative inline-flex ${className}`}>\n      <button\n        type=\"button\"\n        aria-expanded={open}\n        aria-label={`Source ${citation.id}: ${citation.title}`}\n        onMouseEnter={() => setOpen(true)}\n        onMouseLeave={() => setOpen(false)}\n        onFocus={() => setOpen(true)}\n        onBlur={() => setOpen(false)}\n        className=\"mx-0.5 inline-flex h-[15px] w-[15px] translate-y-[-2px] items-center justify-center rounded-full bg-zinc-200 text-[9px] font-semibold text-zinc-700 transition-colors hover:bg-zinc-900 hover:text-white dark:bg-zinc-700 dark:text-zinc-300 dark:hover:bg-zinc-100 dark:hover:text-zinc-900\"\n      >\n        {citation.id}\n      </button>\n\n      {open && (\n        <a\n          href={citation.url}\n          target=\"_blank\"\n          rel=\"noreferrer noopener\"\n          onMouseEnter={() => setOpen(true)}\n          onMouseLeave={() => setOpen(false)}\n          className=\"absolute left-1/2 top-full z-30 mt-2 w-64 -translate-x-1/2 rounded-xl border border-zinc-200 bg-white p-3 shadow-lg dark:border-zinc-700 dark:bg-zinc-800\"\n        >\n          <p className=\"text-sm font-medium leading-snug text-zinc-800 dark:text-zinc-100\">\n            {citation.title}\n          </p>\n          <p className=\"mt-1 flex items-center gap-1 text-xs text-zinc-500 dark:text-zinc-400\">\n            <span className=\"truncate\">{host}</span>\n            <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" width=\"11\" height=\"11\" className=\"shrink-0\">\n              <path d=\"M7 17 17 7M7 7h10v10\" />\n            </svg>\n          </p>\n          {citation.snippet && (\n            <p className=\"mt-1.5 text-[13px] leading-5 text-zinc-500 dark:text-zinc-400\">\n              {citation.snippet}\n            </p>\n          )}\n        </a>\n      )}\n    </span>\n  );\n}\n\n/* ------------------------------------------------------------------ */\n/* CitationList — numbered source list rendered under an answer        */\n/* ------------------------------------------------------------------ */\n\nexport function CitationList({\n  citations,\n  className = \"\",\n  linkable = true,\n}: {\n  citations: Citation[];\n  className?: string;\n  /** False inside a card already wrapped in <a>, where nested anchors are invalid HTML. */\n  linkable?: boolean;\n}) {\n  if (citations.length === 0) return null;\n  return (\n    <div className={`space-y-1.5 ${className}`}>\n      <p className=\"text-xs font-medium uppercase tracking-wide text-zinc-500 dark:text-zinc-400\">Sources</p>\n      <ol className=\"space-y-1\">\n        {citations.map((c) => {\n          const titleClass =\n            \"truncate text-zinc-600 transition-colors hover:text-zinc-900 hover:underline dark:text-zinc-300 dark:hover:text-zinc-100\";\n          return (\n            <li key={c.id} className=\"flex items-baseline gap-2 text-sm\">\n              <span className=\"w-4 shrink-0 text-right text-xs tabular-nums text-zinc-500 dark:text-zinc-400\">\n                {c.id}\n              </span>\n              {linkable ? (\n                <a\n                  href={c.url}\n                  target=\"_blank\"\n                  rel=\"noreferrer noopener\"\n                  className={titleClass}\n                >\n                  {c.title}\n                </a>\n              ) : (\n                <span className={titleClass}>{c.title}</span>\n              )}\n            </li>\n          );\n        })}\n      </ol>\n    </div>\n  );\n}\n","type":"registry:ui","target":"components/ui/citation-ui.tsx"}]}