{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sources-list",
  "title": "Sources List",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "The 'Used N sources' footer of an AI answer: a stack of site icons that opens a numbered list or card grid, with show-all, a highlighted row for hover sync and stable row ids.",
  "dependencies": [
    "motion@^12",
    "lucide-react",
    "radix-ui"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/citation.json",
    "https://ui.ballmac.com/r/i18n.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/sources-list.tsx",
      "content": "// Ballmac UI: Sources List. https://ui.ballmac.com/components/sources-list\n\"use client\"\n\nimport * as React from \"react\"\nimport { ChevronDown } from \"lucide-react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\nimport { Collapsible as CollapsiblePrimitive } from \"radix-ui\"\n\nimport { SourceFavicon, hostOf, type CitationSource } from \"@/components/ballmac/citation\"\nimport { cn } from \"@/lib/utils\"\nimport { useMessages } from \"@/lib/ballmac/i18n\"\n\ntype SourcesListProps = Omit<React.ComponentProps<\"div\">, \"onChange\" | \"title\"> & {\n  /** The pages the answer drew on, in citation order: the first is [1]. */\n  sources: CitationSource[]\n  /** \"list\" is one compact row per source, \"cards\" is a grid with excerpts. */\n  variant?: \"list\" | \"cards\"\n  /** Heading. The count is added for you. */\n  title?: string\n  /** Controlled open state. */\n  open?: boolean\n  /** Initial open state when uncontrolled. */\n  defaultOpen?: boolean\n  /** Called when the list opens or closes. */\n  onOpenChange?: (open: boolean) => void\n  /** Turn off the disclosure and always show the list. */\n  collapsible?: boolean\n  /** How many sources show before \"Show all\". */\n  visibleCount?: number\n  /** One-based number of the source to emphasize, for example while its citation is hovered. */\n  highlight?: number | null\n  /** Id prefix for each source row (`<idPrefix>-1`). Lets a citation link scroll to its row. */\n  idPrefix?: string\n}\n\nfunction SourcesList({\n  sources,\n  variant = \"list\",\n  title,\n  open: openProp,\n  defaultOpen = false,\n  onOpenChange,\n  collapsible = true,\n  visibleCount = 4,\n  highlight = null,\n  idPrefix,\n  className,\n  ...props\n}: SourcesListProps) {\n  const msg = useMessages()\n  title ??= msg(\"sources-list.title\", \"Sources\")\n  const reduce = useReducedMotion()\n  const autoId = React.useId()\n  const prefix = idPrefix ?? `sources${autoId.replace(/:/g, \"\")}`\n  const [internalOpen, setInternalOpen] = React.useState(defaultOpen)\n  const open = collapsible ? (openProp ?? internalOpen) : true\n  const [all, setAll] = React.useState(false)\n  const shown = all ? sources : sources.slice(0, visibleCount)\n  const hidden = sources.length - shown.length\n\n  function setOpen(next: boolean) {\n    setInternalOpen(next)\n    onOpenChange?.(next)\n  }\n\n  const count = `${sources.length} ${sources.length === 1 ? \"source\" : \"sources\"}`\n\n  const rows = (\n    <ol\n      aria-label={title}\n      className={cn(variant === \"cards\" ? \"grid gap-2 sm:grid-cols-2\" : \"grid gap-0.5\")}\n    >\n      <AnimatePresence initial={false}>\n        {shown.map((source, i) => {\n          const n = i + 1\n          const active = highlight === n\n          return (\n            <motion.li\n              key={source.url}\n              layout={reduce ? false : \"position\"}\n              initial={reduce ? false : { opacity: 0, y: 6 }}\n              animate={{ opacity: 1, y: 0 }}\n              exit={{ opacity: 0 }}\n              transition={{ duration: 0.2, delay: reduce ? 0 : Math.min(Math.max(i - visibleCount, 0), 6) * 0.03 }}\n              className=\"min-w-0\"\n            >\n              <a\n                id={`${prefix}-${n}`}\n                href={source.url}\n                target=\"_blank\"\n                rel=\"noreferrer noopener\"\n                data-active={active || undefined}\n                className={cn(\n                  \"group/source flex min-w-0 gap-3 rounded-lg outline-none transition-[background-color,box-shadow] duration-150 focus-visible:ring-[3px] focus-visible:ring-ring/50 motion-reduce:transition-none\",\n                  variant === \"cards\"\n                    ? \"h-full border bg-card p-3 hover:border-foreground/25 hover:bg-accent/50\"\n                    : \"items-center px-2.5 py-2 hover:bg-accent/70\",\n                  active && \"bg-accent ring-1 ring-foreground/25\"\n                )}\n              >\n                <span\n                  aria-hidden=\"true\"\n                  className=\"flex h-5 min-w-5 shrink-0 items-center justify-center rounded-md bg-muted px-1 font-mono text-[11px] text-muted-foreground tabular-nums\"\n                >\n                  {n}\n                </span>\n                <span className=\"grid min-w-0 flex-1 gap-0.5\">\n                  <span className=\"flex min-w-0 items-center gap-1.5 text-xs text-muted-foreground\">\n                    <SourceFavicon source={source} className=\"size-4 text-[9px]\" />\n                    <span className=\"truncate\">{source.site ?? hostOf(source.url)}</span>\n                    {source.date && <span className=\"shrink-0 before:me-1.5 before:content-['·']\">{source.date}</span>}\n                  </span>\n                  <span\n                    className={cn(\n                      \"text-sm font-medium text-foreground group-hover/source:underline group-hover/source:underline-offset-4\",\n                      variant === \"cards\" ? \"line-clamp-2\" : \"truncate\"\n                    )}\n                  >\n                    <span className=\"sr-only\">{`Source ${n}:`}</span>{\" \"}\n                    {source.title}\n                  </span>\n                  {variant === \"cards\" && source.snippet && (\n                    <span className=\"line-clamp-2 text-[13px] leading-5 text-muted-foreground\">{source.snippet}</span>\n                  )}\n                </span>\n              </a>\n            </motion.li>\n          )\n        })}\n      </AnimatePresence>\n    </ol>\n  )\n\n  const more = hidden > 0 && (\n    <button\n      type=\"button\"\n      onClick={() => setAll(true)}\n      className=\"mt-1 ms-2 inline-flex h-8 items-center rounded-md px-2 text-[13px] font-medium text-muted-foreground outline-none hover:bg-accent hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50\"\n    >\n      {msg(\"sources-list.showAll\", \"Show all\")} {sources.length}\n    </button>\n  )\n\n  if (!collapsible) {\n    return (\n      <div data-slot=\"sources-list\" className={cn(\"w-full\", className)} {...props}>\n        <p className=\"mb-1.5 px-2.5 text-sm font-medium text-foreground\">\n          {title} <span className=\"font-normal text-muted-foreground tabular-nums\">{sources.length}</span>\n        </p>\n        {rows}\n        {more}\n      </div>\n    )\n  }\n\n  const stack = sources.slice(0, 4)\n  return (\n    <CollapsiblePrimitive.Root\n      data-slot=\"sources-list\"\n      open={open}\n      onOpenChange={setOpen}\n      className={cn(\"w-full rounded-xl border bg-card text-card-foreground\", className)}\n      {...props}\n    >\n      <CollapsiblePrimitive.Trigger\n        data-slot=\"sources-trigger\"\n        className=\"group flex min-h-11 w-full items-center gap-3 rounded-xl px-3 py-2 text-start text-sm outline-none transition-colors duration-150 hover:bg-accent/50 focus-visible:ring-[3px] focus-visible:ring-ring/50 motion-reduce:transition-none\"\n      >\n        <span aria-hidden=\"true\" className=\"flex -space-x-1.5\">\n          {stack.map((s, i) => (\n            <SourceFavicon\n              key={s.url}\n              source={s}\n              className=\"size-6 rounded-full border-2 border-card text-[10px]\"\n              style={{ zIndex: stack.length - i }}\n            />\n          ))}\n        </span>\n        <span className=\"font-medium text-foreground\">{title}</span>\n        <span className=\"text-muted-foreground tabular-nums\">{count}</span>\n        <ChevronDown\n          aria-hidden=\"true\"\n          className=\"ms-auto size-4 shrink-0 text-muted-foreground transition-transform duration-200 group-data-[state=open]:rotate-180 motion-reduce:transition-none\"\n        />\n      </CollapsiblePrimitive.Trigger>\n      <CollapsiblePrimitive.Content className=\"overflow-hidden data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0 motion-reduce:animate-none\">\n        <div className=\"border-t p-1.5\">\n          {rows}\n          {more}\n        </div>\n      </CollapsiblePrimitive.Content>\n    </CollapsiblePrimitive.Root>\n  )\n}\n\nexport { SourcesList, type SourcesListProps }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/sources-list.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "ai",
      "sources",
      "references",
      "citations",
      "disclosure"
    ],
    "version": "1.0.0",
    "updated": "2026-09-30",
    "ai": {
      "summary": "sources is the array used by citations, in order. variant is list | cards. collapsible={false} keeps it open. highlight={n} emphasizes row n, and each row has id `${idPrefix}-${n}` so a citation can link to it.",
      "whenToUse": [
        "The end of a grounded answer",
        "A side panel of everything the assistant read"
      ],
      "whenNotToUse": [
        "Inline markers; use citation",
        "Generic link lists"
      ],
      "composesWith": [
        "citation",
        "ai-message",
        "collapsible"
      ],
      "a11y": [
        {
          "keys": "Enter / Space",
          "action": "Opens or closes the list from its header"
        },
        {
          "keys": "Tab",
          "action": "Moves through the source links"
        },
        {
          "keys": "Screen readers",
          "action": "An ordered list named Sources; each link reads 'Source 2: Title'"
        }
      ],
      "customization": [
        "variant",
        "visibleCount",
        "highlight",
        "idPrefix",
        "open / defaultOpen / onOpenChange"
      ]
    },
    "examples": [
      "sources-list-demo",
      "sources-list-cards"
    ],
    "url": "https://ui.ballmac.com/components/sources-list"
  },
  "categories": [
    "ai"
  ],
  "type": "registry:ui"
}