{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "citation",
  "type": "registry:ui",
  "title": "Citation",
  "description": "Inline numbered citation pill that links to its source and previews favicon, title, domain and snippet on hover/focus, plus a Sources strip or list.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "https://kuro.abstergo.fyi/r/kura-theme.json",
    "https://kuro.abstergo.fyi/r/popover.json",
    "https://kuro.abstergo.fyi/r/use-controllable-state.json",
    "utils"
  ],
  "files": [
    {
      "path": "registry/kura/citation/citation.tsx",
      "type": "registry:ui",
      "target": "components/kura/citation/citation.tsx",
      "content": "\"use client\";\n\nimport { ArrowUpRight } from \"lucide-react\";\nimport * as React from \"react\";\nimport { useControllableState } from \"@/hooks/kura/use-controllable-state\";\nimport { cn } from \"@/lib/utils\";\nimport { Popover, PopoverAnchor, PopoverContent } from \"@/components/kura/popover\";\n\nexport interface CitationSource {\n  /** Page title. */\n  title: string;\n  /** Canonical URL; the domain is derived from it. */\n  url: string;\n  /** Short excerpt the model relied on. */\n  snippet?: string;\n  /** Favicon URL. Without one, a monogram tile is shown (no third-party favicon service is called). */\n  favicon?: string;\n  /** Publisher name shown instead of the domain, e.g. \"React Docs\". */\n  siteName?: string;\n  /** Publication date as display text, e.g. \"Mar 2025\". */\n  date?: string;\n}\n\nfunction isFocusVisible(el: Element): boolean {\n  try {\n    return el.matches(\":focus-visible\");\n  } catch {\n    return true;\n  }\n}\n\nexport function sourceDomain(url: string): string {\n  try {\n    return new URL(url).hostname.replace(/^www\\./, \"\");\n  } catch {\n    return url;\n  }\n}\n\nfunction Favicon({ source, className }: { source: CitationSource; className?: string }) {\n  const [broken, setBroken] = React.useState(false);\n  const domain = sourceDomain(source.url);\n  return (\n    <span\n      aria-hidden\n      data-slot=\"citation-favicon\"\n      className={cn(\n        \"grid size-4 shrink-0 place-items-center overflow-hidden rounded-kura-xs bg-kura-surface-raised font-semibold text-[9px] text-kura-fg-muted uppercase shadow-[0_0_0_1px_var(--kura-border-subtle)]\",\n        className,\n      )}\n    >\n      {source.favicon && !broken ? (\n        <img\n          src={source.favicon}\n          alt=\"\"\n          className=\"size-full object-cover\"\n          onError={() => setBroken(true)}\n        />\n      ) : (\n        (source.siteName ?? domain).charAt(0)\n      )}\n    </span>\n  );\n}\n\n// ------------------------------------------------------------------------------------------------\n// Card\n// ------------------------------------------------------------------------------------------------\n\nexport interface CitationCardProps extends React.ComponentProps<\"div\"> {\n  /** The source to preview. */\n  source: CitationSource;\n  /** Citation number shown in the corner. */\n  index?: number;\n}\n\n/** The preview body: favicon, site, title (a link), snippet. Used in the hover card; reusable in sidebars. */\nexport function CitationCard({ source, index, className, ...props }: CitationCardProps) {\n  const domain = sourceDomain(source.url);\n  return (\n    <div\n      data-slot=\"citation-card\"\n      className={cn(\"flex flex-col gap-1.5 font-kura-sans\", className)}\n      {...props}\n    >\n      <div className=\"flex items-center gap-2 text-kura-fg-subtle text-xs\">\n        <Favicon source={source} />\n        <span className=\"min-w-0 flex-1 truncate\">\n          {source.siteName ?? domain}\n          {source.date && <span className=\"text-kura-fg-subtle\"> · {source.date}</span>}\n        </span>\n        {index != null && <span className=\"font-kura-mono tabular-nums\">{index}</span>}\n      </div>\n      <a\n        href={source.url}\n        target=\"_blank\"\n        rel=\"noreferrer\"\n        className=\"group/title -mx-1 line-clamp-2 rounded-kura-xs px-1 font-medium text-kura-fg text-sm leading-snug outline-none hover:underline hover:decoration-kura-border-strong hover:underline-offset-2 focus-visible:shadow-kura-focus\"\n      >\n        {source.title}\n        <ArrowUpRight\n          aria-hidden\n          className=\"ml-0.5 inline size-3.5 align-[-2px] text-kura-fg-subtle transition-transform duration-(--kura-duration-fast) ease-kura-out group-hover/title:translate-x-px group-hover/title:-translate-y-px motion-reduce:transition-none\"\n        />\n      </a>\n      {source.snippet && (\n        <p className=\"line-clamp-3 text-[13px] text-kura-fg-muted leading-relaxed\">\n          {source.snippet}\n        </p>\n      )}\n    </div>\n  );\n}\n\n// ------------------------------------------------------------------------------------------------\n// Inline citation\n// ------------------------------------------------------------------------------------------------\n\nexport interface CitationProps extends Omit<React.ComponentProps<\"a\">, \"href\" | \"children\"> {\n  /** 1-based citation number shown in the pill. */\n  index: number;\n  /** The cited source. */\n  source: CitationSource;\n  /** Pill label: the number (default) or the source's domain/site name. */\n  display?: \"number\" | \"domain\";\n  /** Hover delay before the preview opens, in ms. Default 250. */\n  openDelay?: number;\n  /** Preview open (controlled). */\n  open?: boolean;\n  onOpenChange?: (open: boolean) => void;\n}\n\n/**\n * Inline numbered citation pill. It is a real link to the source; hovering or keyboard-focusing\n * it opens a preview card with favicon, title, domain and snippet.\n */\nexport function Citation({\n  index,\n  source,\n  display = \"number\",\n  openDelay = 250,\n  open: openProp,\n  onOpenChange,\n  className,\n  onPointerEnter,\n  onPointerLeave,\n  onFocus,\n  onBlur,\n  ...props\n}: CitationProps) {\n  const [open, setOpen] = useControllableState({\n    value: openProp,\n    defaultValue: false,\n    onChange: onOpenChange,\n  });\n  const timer = React.useRef<ReturnType<typeof setTimeout>>(undefined);\n  React.useEffect(() => () => clearTimeout(timer.current), []);\n  const schedule = (next: boolean, delay: number) => {\n    clearTimeout(timer.current);\n    timer.current = setTimeout(() => setOpen(next), delay);\n  };\n  const domain = sourceDomain(source.url);\n  const label = display === \"domain\" ? (source.siteName ?? domain) : String(index);\n\n  return (\n    <Popover open={open} onOpenChange={setOpen}>\n      <PopoverAnchor asChild>\n        <a\n          href={source.url}\n          target=\"_blank\"\n          rel=\"noreferrer\"\n          data-slot=\"citation\"\n          data-state={open ? \"open\" : \"closed\"}\n          aria-label={`Source ${index}: ${source.title} (${domain})`}\n          onPointerEnter={(e) => {\n            onPointerEnter?.(e);\n            if (e.pointerType !== \"touch\") schedule(true, openDelay);\n          }}\n          onPointerLeave={(e) => {\n            onPointerLeave?.(e);\n            schedule(false, 150);\n          }}\n          onFocus={(e) => {\n            onFocus?.(e);\n            if (isFocusVisible(e.currentTarget)) schedule(true, 0);\n          }}\n          onBlur={(e) => {\n            onBlur?.(e);\n            schedule(false, 100);\n          }}\n          className={cn(\n            \"relative -top-[0.1em] mr-px ml-1 inline-flex h-[1.3em] min-w-[1.3em] items-center justify-center gap-1 rounded-full bg-kura-hover px-[0.4em] align-middle font-kura-sans font-medium text-[0.72em] text-kura-fg-muted tabular-nums leading-none no-underline outline-none\",\n            \"transition-[background-color,color,box-shadow] duration-(--kura-duration-fast) ease-kura-out hover:bg-kura-active hover:text-kura-fg focus-visible:shadow-kura-focus data-[state=open]:bg-kura-active data-[state=open]:text-kura-fg\",\n            className,\n          )}\n          {...props}\n        >\n          {display === \"domain\" && (\n            <Favicon source={source} className=\"-ml-0.5 size-[1.15em] rounded-full\" />\n          )}\n          {label}\n        </a>\n      </PopoverAnchor>\n      <PopoverContent\n        side=\"top\"\n        align=\"start\"\n        sideOffset={6}\n        data-slot=\"citation-preview\"\n        className=\"w-80 p-3\"\n        onOpenAutoFocus={(e) => e.preventDefault()}\n        onCloseAutoFocus={(e) => e.preventDefault()}\n        onPointerEnter={() => clearTimeout(timer.current)}\n        onPointerLeave={() => schedule(false, 150)}\n      >\n        <CitationCard source={source} index={index} />\n      </PopoverContent>\n    </Popover>\n  );\n}\n\n// ------------------------------------------------------------------------------------------------\n// Sources\n// ------------------------------------------------------------------------------------------------\n\nexport interface SourcesProps extends React.ComponentProps<\"div\"> {\n  /** Sources in citation order (index = position + 1). */\n  sources: readonly CitationSource[];\n  /** `strip` = a horizontally scrolling row of chips; `list` = a numbered list with titles and snippets. Default \"strip\". */\n  variant?: \"strip\" | \"list\";\n  /** Heading text. Default “Sources”. Pass null to hide it. */\n  label?: React.ReactNode;\n}\n\n/** The sources behind an answer, as a compact chip strip or a numbered list. */\nexport function Sources({\n  sources,\n  variant = \"strip\",\n  label = \"Sources\",\n  className,\n  ...props\n}: SourcesProps) {\n  const id = React.useId();\n  return (\n    <div\n      data-slot=\"citation-sources\"\n      data-variant={variant}\n      className={cn(\"flex min-w-0 flex-col gap-2 font-kura-sans\", className)}\n      {...props}\n    >\n      {label != null && (\n        <div id={id} className=\"flex items-center gap-2 font-medium text-kura-fg-muted text-xs\">\n          {label}\n          <span className=\"font-kura-mono text-kura-fg-subtle tabular-nums\">{sources.length}</span>\n        </div>\n      )}\n      <ol\n        aria-labelledby={label != null ? id : undefined}\n        aria-label={label == null ? \"Sources\" : undefined}\n        className={cn(\n          variant === \"strip\"\n            ? \"-mx-1 -my-1 flex snap-x gap-2 overflow-x-auto px-1 py-1 [mask-image:linear-gradient(to_right,black_calc(100%-2rem),transparent)] [scrollbar-width:none]\"\n            : \"flex flex-col gap-1\",\n        )}\n      >\n        {sources.map((source, i) => {\n          const domain = sourceDomain(source.url);\n          return (\n            <li key={source.url + i} data-slot=\"citation-source\" className=\"shrink-0 snap-start\">\n              {variant === \"strip\" ? (\n                <a\n                  href={source.url}\n                  target=\"_blank\"\n                  rel=\"noreferrer\"\n                  className=\"flex w-52 flex-col gap-1.5 rounded-kura-lg bg-kura-surface p-2.5 shadow-kura-xs outline-none transition-[background-color,box-shadow] duration-(--kura-duration-fast) ease-kura-out hover:bg-kura-surface-sunken focus-visible:shadow-kura-focus\"\n                >\n                  <span className=\"line-clamp-2 min-h-[2lh] text-[13px] text-kura-fg leading-snug\">\n                    {source.title}\n                  </span>\n                  <span className=\"flex items-center gap-1.5 text-kura-fg-subtle text-xs\">\n                    <Favicon source={source} className=\"size-3.5\" />\n                    <span className=\"min-w-0 flex-1 truncate\">{source.siteName ?? domain}</span>\n                    <span className=\"font-kura-mono tabular-nums\">{i + 1}</span>\n                  </span>\n                </a>\n              ) : (\n                <a\n                  href={source.url}\n                  target=\"_blank\"\n                  rel=\"noreferrer\"\n                  className=\"-mx-2 flex gap-3 rounded-kura-md px-2 py-2 outline-none transition-colors duration-(--kura-duration-fast) ease-kura-out hover:bg-kura-hover focus-visible:shadow-kura-focus\"\n                >\n                  <span className=\"w-4 shrink-0 pt-px text-right font-kura-mono text-kura-fg-subtle text-xs tabular-nums\">\n                    {i + 1}\n                  </span>\n                  <span className=\"flex min-w-0 flex-1 flex-col gap-0.5\">\n                    <span className=\"truncate font-medium text-kura-fg text-sm\">\n                      {source.title}\n                    </span>\n                    <span className=\"flex items-center gap-1.5 text-kura-fg-subtle text-xs\">\n                      <Favicon source={source} className=\"size-3.5\" />\n                      <span className=\"truncate\">{source.siteName ?? domain}</span>\n                      {source.date && <span>· {source.date}</span>}\n                    </span>\n                    {source.snippet && (\n                      <span className=\"line-clamp-2 pt-0.5 text-[13px] text-kura-fg-muted leading-relaxed\">\n                        {source.snippet}\n                      </span>\n                    )}\n                  </span>\n                </a>\n              )}\n            </li>\n          );\n        })}\n      </ol>\n    </div>\n  );\n}\n"
    },
    {
      "path": "registry/kura/citation/index.ts",
      "type": "registry:ui",
      "target": "components/kura/citation/index.ts",
      "content": "export {\n  Citation,\n  CitationCard,\n  type CitationCardProps,\n  type CitationProps,\n  type CitationSource,\n  Sources,\n  type SourcesProps,\n  sourceDomain,\n} from \"./citation\";\n"
    }
  ],
  "categories": [
    "conversation",
    "citation",
    "sources",
    "rag",
    "references"
  ],
  "docs": "Docs: https://kuro.abstergo.fyi/components/citation · Markdown for agents: https://kuro.abstergo.fyi/components/citation.md",
  "meta": {
    "kura": {
      "status": "beta",
      "whenToUse": [
        "Attribute sentences of a model's answer to retrieved sources (RAG, web search, file search) with inline [1] [2] pills.",
        "Preview a source without leaving the answer: hover or keyboard-focus the pill to see title, domain and the snippet used.",
        "List all sources behind an answer under it, as a compact chip strip or a numbered list (`Sources`)."
      ],
      "whenNotToUse": [
        "Plain links in prose with no source metadata — use an <a>.",
        "Footnotes in long-form documents that must print — use real footnotes.",
        "Showing the search tool call itself (query, results JSON) — use tool-call."
      ]
    }
  }
}
