{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sources",
  "type": "registry:ui",
  "title": "Sources",
  "description": "Where an answer came from: Sources holds a turn's sources and numbers them once, Citation is a quiet numbered chip in the running text that opens its source and previews it in a hover card (the site, the title, the passage, when it was read), and SourceList lists them under the answer or as what a search call found. Resting on a citation lights its row, and resting on a row lights its citations. A source the model gives a url that is not http or https shows as text.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://littmd.vercel.app/r/litt.json",
    "https://littmd.vercel.app/r/hover-card.json",
    "https://littmd.vercel.app/r/icons.json",
    "https://littmd.vercel.app/r/motion.json"
  ],
  "files": [
    {
      "path": "registry/litt/ui/sources.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n// @litt/sources 2026.11.06. What changed: https://littmd.vercel.app/changelog.md\n\n/**\n * Sources: where an answer came from (components.md, \"Sources\"; proposals/agents-2.md). Sources holds a turn's sources\n * and numbers them once, Citation is the numbered mark a markdown renderer puts in the running text, and SourceList is\n * the same sources as rows: under the answer, or inside the search call that found them. One Source feeds all three.\n *\n * - Anatomy, the mark: a 16px chip on the line, min 16 wide with 4px of side padding at the control radius, the\n *   number at the caption size (text weight, tabular) in fg-muted on fg 6%, 4px after the word. It never grows the line\n *   box. It is flat (R3): a reference you follow, not an object you move, so it never rises, takes no underline and no\n *   accent. It is a link to the source in a new tab (rel noreferrer), as citations are everywhere, and the HoverCard's\n *   trigger: after the pointer rests 500ms, or at once on keyboard focus, the card grows from it (the responsive tier)\n *   with the kind's icon and the site (small, fg-muted), the title (small, title weight, fg, two lines) as the link,\n *   the passage in quotes (small, fg-muted, four lines, no rail and no tint) and the read words (caption, fg-subtle).\n *   `quote` replaces the passage for one mark, because one page can back two claims.\n * - Anatomy, the list: an ol, bare in the turn. A heading holds the label (small, title weight) and the count (caption,\n *   tabular). Each row is one link to the source in a new tab, at least 44px, two lines: the title (small, fg, cut at the\n *   end; a file's title in the mono) over the site and the read words (caption, fg-subtle). A 16px lead column holds the\n *   number chip, the mark's look but not a control, where a Tool call row shows its mark; the row's fill hangs 8px into\n *   the gutter so its text keeps the turn's edge. With `sources` (what a search found) a row the turn cites wears its\n *   number and an uncited one shows its kind's icon in fg-subtle with hidden \"Not cited\".\n * - Flat: the list is a grey card at the surface radius with no frame, its rows concentric inside it, and the number chip\n *   a white pill.\n * - The moment: rest on a mark and its row lights (fg 6%), and so does every other mark of that source (fg on fg 12%).\n *   Rest on a row and its marks light. Color only, on the instant tier; nothing moves. The light is a small store in\n *   Sources with two fields, `hovered` (the pointer) and `focused` (the keyboard), merged where it is read\n *   (hovered ?? focused), so two hands never fight over one field, and marks and rows subscribe by id through\n *   useSyncExternalStore, so pointing re-renders only the marks and rows of the ids involved.\n * - Fun mode (motion/fun.tsx, a trial, off unless a <FunMode> is on): resting on a row pops its marks in reading order\n *   (scale 1.2 to 1 on the pop spring, a wave apart), and each transform is cleared when it lands, so the rest frame is\n *   Calm's exactly. Reduced motion turns it off.\n * - The boundary: parseSources is the one place a Source is checked. A later duplicate id is dropped, an href is kept\n *   only for http and https (a model's javascript: url shows as text), the site is read from the host when it is\n *   missing, and the number is the place in `sources`. `citationId` reads the `#cite-<id>` link a model writes and\n *   `unlinkCitations` strips those links for Copy, so the scheme lives in one file. A mark whose id the Sources does not\n *   hold, or a mark outside a Sources, renders nothing and warns once in development: a mark can stream in before its\n *   source does.\n * - Keys and assistive tech: marks and rows are links in the tab order. A mark is named \"Source 2: <title>\" and\n *   described by its site and \"opens in a new tab\"; focus opens the card at once and Esc closes it. The list is an ol\n *   named by the label (kept for assistive tech with `hideLabel`); a row reads its number, title, site, read words and\n *   \"Opens in a new tab\". The card is a preview only: everything in it is on the row or on the page the link opens.\n * - Motion: only the chip's and the row's colors fade, on the instant tier. The card is the HoverCard's (responsive\n *   tier, the exit token out).\n * - Touch: the card never opens (HoverCard's rule); the number pairs the mark with its row. A mark's hit area is 44px\n *   tall and 24px wide on a coarse pointer (an after: box), because two marks sit 4px apart in a line: the named\n *   exception to R9's 44 in width that prose links take. Rows are 44px, and a pressed row takes its hover fill.\n * - Disabled or read-only: nothing to disable. A source with no safe url is text: its mark is a plain chip and its row\n *   has no link.\n * - Forced colors: the chip keeps a 1px CanvasText border, and a lit chip or row takes Highlight and HighlightText.\n */\nimport { createContext, useContext, useEffect, useId, useMemo, useRef, useState, useSyncExternalStore, type ReactNode } from \"react\";\nimport { animate } from \"motion/react\";\nimport { HoverCard, HoverCardContent, HoverCardTrigger } from \"./hover-card\";\nimport { FileIcon, GlobeIcon } from \"./icons\";\nimport { useFun } from \"../../lib/litt/motion\";\n\n/* ----------------------------------------------------------------- data */\n\n/** One page or file an agent found or read: the same shape for a citation's card, the list under an answer and a search call's results. */\nexport type Source = {\n  /** A stable id the answer cites (\"geist\"). Unique inside one Sources; a later duplicate is dropped. */\n  id: string;\n  /** The page's title, as the agent read it. A file's title is its path. */\n  title: string;\n  /** Where it lives. Only http and https become links; any other url shows the title as text. */\n  url: string;\n  /** The site in words (\"nextjs.org\"). Without it, the url's host without \"www.\". */\n  site?: string;\n  /** The line the answer leans on, quoted in the card. A Citation's `quote` replaces it for that one mark. */\n  passage?: string;\n  /** When the agent read it, as words: \"2 minutes ago\", \"27 Oct, 14:02\". The card and the row say \"Read 2 minutes ago\". */\n  readAt?: string;\n  /** web shows the globe; file shows the file icon and sets the title in the mono. */\n  kind?: \"web\" | \"file\";\n};\n\n/** A Source after the boundary: the link is safe or absent, the host is a word, and the number is its place. */\ntype ParsedSource = {\n  id: string;\n  /** 1-based place in `sources`: the number a mark shows and a row wears. */\n  n: number;\n  title: string;\n  /** The url when it is http or https, else null: the title then shows as text. */\n  href: string | null;\n  site: string | null;\n  passage: string | null;\n  readAt: string | null;\n  kind: \"web\" | \"file\";\n};\n\n/** The url when a browser would open it as a page. A model's `javascript:` or a bare path is not a link. */\nfunction safeHref(url: string): string | null {\n  try {\n    const protocol = new URL(url).protocol;\n    return protocol === \"http:\" || protocol === \"https:\" ? url : null;\n  } catch {\n    return null;\n  }\n}\n\n/** \"nextjs.org\" from \"https://www.nextjs.org/docs\". */\nconst hostOf = (href: string | null) => (href ? new URL(href).hostname.replace(/^www\\./, \"\") : null);\n\n/** The one place a Source is checked: duplicates dropped, hrefs kept only for http and https, the site from the host, numbered by position. */\nfunction parseSources(raw: readonly Source[]): ParsedSource[] {\n  const seen = new Set<string>();\n  const out: ParsedSource[] = [];\n  for (const s of raw) {\n    if (seen.has(s.id)) continue;\n    seen.add(s.id);\n    const href = safeHref(s.url);\n    out.push({\n      id: s.id,\n      n: out.length + 1,\n      title: s.title,\n      href,\n      site: s.site ?? hostOf(href),\n      passage: s.passage ?? null,\n      readAt: s.readAt ?? null,\n      kind: s.kind ?? \"web\",\n    });\n  }\n  return out;\n}\n\n/** The link a model writes to cite a source: `[2](#cite-geist)`. Read by citationId, removed by unlinkCitations. */\nconst CITE = \"#cite-\";\n\n/**\n * The source id a markdown link names: \"#cite-geist\" gives \"geist\", and any other href gives null. Call it where your\n * markdown renderer meets a link, and render a Citation for the ids it returns.\n * @param href The link's href, as the renderer hands it to you.\n */\nexport function citationId(href: string | undefined): string | null {\n  if (!href || !href.startsWith(CITE)) return null;\n  const id = href.slice(CITE.length);\n  return id ? id : null;\n}\n\n/**\n * Turns \"[2](#cite-geist)\" into \"[2]\" throughout a markdown string, for Copy and exports, so no fragment link leaves the\n * page. The number stays and the list under the answer explains it.\n * @param markdown The answer as the model wrote it.\n */\nexport function unlinkCitations(markdown: string): string {\n  return markdown.replace(/\\[([^\\]]*)\\]\\(#cite-[^)\\s]+\\)/g, \"[$1]\");\n}\n\n/* ----------------------------------------------------------------- the light */\n\ntype Actor = \"mark\" | \"row\";\n/** What one hand points at: the source, and from which side. */\ntype Light = { id: string; from: Actor } | null;\n/** The pointer and the keyboard each own a field. They are merged only where the light is read. */\ntype Pointing = { hovered: Light; focused: Light };\ntype Hand = keyof Pointing;\n\ntype LightStore = {\n  get: () => Pointing;\n  subscribe: (listener: () => void) => () => void;\n  /** A hand is now on a mark or a row of `id`. */\n  point: (hand: Hand, id: string, from: Actor) => void;\n  /** A hand left `id`. It clears the field only when that hand still holds this light, so a later pointing is never undone. */\n  release: (hand: Hand, id: string, from: Actor) => void;\n  /** A mark registers its element; the removal is returned. */\n  register: (id: string, el: HTMLElement) => () => void;\n  /** The marks of `id` in reading order, for fun mode's wave. */\n  marksOf: (id: string) => HTMLElement[];\n};\n\nconst litBy = (p: Pointing): Light => p.hovered ?? p.focused;\n\nfunction createLightStore(): LightStore {\n  let state: Pointing = { hovered: null, focused: null };\n  const listeners = new Set<() => void>();\n  const marks = new Map<string, Set<HTMLElement>>();\n  const set = (hand: Hand, light: Light) => {\n    state = { ...state, [hand]: light };\n    listeners.forEach((l) => l());\n  };\n  const holds = (hand: Hand, id: string, from: Actor) => state[hand]?.id === id && state[hand]?.from === from;\n  return {\n    get: () => state,\n    subscribe: (listener) => {\n      listeners.add(listener);\n      return () => listeners.delete(listener);\n    },\n    point: (hand, id, from) => {\n      if (!holds(hand, id, from)) set(hand, { id, from });\n    },\n    release: (hand, id, from) => {\n      if (holds(hand, id, from)) set(hand, null);\n    },\n    register: (id, el) => {\n      const set = marks.get(id) ?? new Set();\n      set.add(el);\n      marks.set(id, set);\n      return () => {\n        set.delete(el);\n      };\n    },\n    marksOf: (id) =>\n      [...(marks.get(id) ?? [])].sort((a, b) => (a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1)),\n  };\n}\n\n/** True while a hand points at `id`, from either side. It re-renders only when this id's answer flips. */\nfunction useLit(store: LightStore, id: string): boolean {\n  return useSyncExternalStore(\n    store.subscribe,\n    () => litBy(store.get())?.id === id,\n    () => false,\n  );\n}\n\n/* ----------------------------------------------------------------- the provider */\n\ntype SourcesContextValue = {\n  list: readonly ParsedSource[];\n  byId: ReadonlyMap<string, ParsedSource>;\n  store: LightStore;\n};\n\nconst SourcesContext = createContext<SourcesContextValue | null>(null);\n\nexport type SourcesProps = {\n  /** The turn's sources, in the order they are numbered. Keep the order while the answer streams, so a number never changes under a reader. */\n  sources: Source[];\n  /** The parts of the turn that show sources: the search call with its results, the MessageText with its Citations, the SourceList. */\n  children: ReactNode;\n};\n\n/**\n * Holds one turn's sources, their numbers and what the reader points at. It draws nothing: put it inside the Message,\n * around the turn's parts, so a search call's results, the marks in the text and the list under it share one numbering.\n */\nexport function Sources({ sources, children }: SourcesProps) {\n  const [store] = useState(createLightStore);\n  const value = useMemo<SourcesContextValue>(() => {\n    const list = parseSources(sources);\n    return { list, byId: new Map(list.map((s) => [s.id, s])), store };\n  }, [sources, store]);\n  return <SourcesContext value={value}>{children}</SourcesContext>;\n}\n\n/* ----------------------------------------------------------------- shared looks */\n\n/** The flat 16px chip with the number, shared by the mark and the row's lead so they read as one thing. Rest or lit, never both. */\nconst CHIP =\n  \"inline-flex h-4 min-w-4 items-center justify-center rounded-litt-control px-1 text-litt-caption font-normal leading-none tabular-nums \" +\n  \"transition-colors duration-(--litt-dur-instant) ease-litt forced-colors:border forced-colors:border-[CanvasText]\";\nconst CHIP_REST = \"bg-[color-mix(in_oklab,var(--litt-fg)_6%,transparent)] text-litt-fg-muted in-[[data-material=flat]]:bg-litt-surface-1\";\nconst CHIP_LIT = \"bg-[color-mix(in_oklab,var(--litt-fg)_12%,transparent)] text-litt-fg forced-colors:bg-[Highlight] forced-colors:text-[HighlightText]\";\nconst chip = (lit: boolean) => CHIP + \" \" + (lit ? CHIP_LIT : CHIP_REST);\n/* MessageText styles every `a` inside it at (0,1,1); the data-citation variant compiles to (0,2,0) and wins. */\nconst MARK_LINK = \"data-citation:no-underline\";\nconst markInk = (lit: boolean) => (lit ? \"data-citation:text-litt-fg\" : \"data-citation:text-litt-fg-muted\");\n/* 44px tall and 24px wide on a finger: marks sit 4px apart in a line, so the box cannot be wider. */\nconst MARK_HIT = \"relative pointer-coarse:after:absolute pointer-coarse:after:-inset-x-1 pointer-coarse:after:-inset-y-3.5\";\nconst FOCUS = \"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-litt-focus\";\n\n/** How many waves apart the marks of one source pop in fun mode. One wave is tuned for neighbouring bars; marks sit a clause apart. */\nconst MARK_WAVES = 6;\n\nfunction KindIcon({ kind, className = \"\" }: { kind: \"web\" | \"file\"; className?: string }) {\n  return kind === \"file\" ? <FileIcon className={\"shrink-0 \" + className} /> : <GlobeIcon className={\"shrink-0 \" + className} />;\n}\n\n/** Whether this is a development build. The bundler replaces `process.env.NODE_ENV`; where it does not, stay quiet. */\ndeclare const process: { env: { NODE_ENV?: string } };\nconst inDevelopment = () => {\n  try {\n    return process.env.NODE_ENV !== \"production\";\n  } catch {\n    return false;\n  }\n};\n\nconst warned = new Set<string>();\n/** Once per id and reason, in development: a mark can stream in before its source, and a host should hear about it once. */\nfunction warnOnce(key: string, message: string) {\n  if (warned.has(key) || !inDevelopment()) return;\n  warned.add(key);\n  console.warn(message);\n}\n\n/* ----------------------------------------------------------------- Citation */\n\nexport type CitationProps = {\n  /** The id of the source it cites, in the nearest Sources. An id the Sources does not hold draws nothing. */\n  source: string;\n  /** The passage this mark leans on, when it is not the source's own. The card quotes it in place of the passage. */\n  quote?: string;\n  /** Extra classes for the mark. */\n  className?: string;\n};\n\n/**\n * The numbered mark in running text: put it where your markdown renderer meets a link whose href citationId reads. A\n * link to the source in a new tab, and a preview card after the pointer rests or at once on keyboard focus. Resting on it\n * lights its row and its sibling marks.\n */\nexport function Citation({ source, quote, className = \"\" }: CitationProps) {\n  const ctx = useContext(SourcesContext);\n  const parsed = ctx?.byId.get(source);\n  if (!ctx) {\n    warnOnce(\"outside:\" + source, `litt: <Citation source=\"${source}\"> sits outside a Sources and draws nothing.`);\n    return null;\n  }\n  if (!parsed) {\n    warnOnce(\"unknown:\" + source, `litt: <Citation source=\"${source}\"> names no source in its Sources and draws nothing.`);\n    return null;\n  }\n  return <Mark source={parsed} quote={quote} store={ctx.store} className={className} />;\n}\n\nfunction Mark({ source, quote, store, className }: { source: ParsedSource; quote?: string; store: LightStore; className: string }) {\n  const { id, n, title, href, site } = source;\n  const lit = useLit(store, id);\n  const fun = useFun();\n  const el = useRef<HTMLElement | null>(null);\n  /* One ref for the link and the plain chip. */\n  const setEl = (node: HTMLElement | null) => {\n    el.current = node;\n  };\n  const describedBy = useId();\n\n  useEffect(() => {\n    const node = el.current;\n    if (!node) return;\n    return store.register(id, node);\n  }, [store, id]);\n\n  /* Fun mode: lit from a row, the marks of that source pop in reading order. The transform is cleared on landing, and\n     when the light goes before that, so the rest frame is always Calm's. */\n  useEffect(() => {\n    const node = el.current;\n    if (!lit || !fun || !node || litBy(store.get())?.from !== \"row\") return;\n    const k = Math.max(0, store.marksOf(id).indexOf(node));\n    const run = animate(node, { scale: [1.2, 1] }, { ...fun.pop, delay: k * fun.wave * MARK_WAVES });\n    const clear = () => {\n      node.style.transform = \"\";\n    };\n    run.then(clear);\n    return () => {\n      run.stop();\n      clear();\n    };\n  }, [lit, fun, store, id]);\n\n  const look = chip(lit) + \" \" + className;\n  const name = `Source ${n}: ${title}`;\n\n  if (!href) {\n    return (\n      <span ref={setEl} data-citation=\"\" data-lit={lit ? \"\" : undefined} className={\"ms-1 align-baseline \" + look}>\n        <span aria-hidden=\"true\">{n}</span>\n        <span className=\"sr-only\">{name}</span>\n      </span>\n    );\n  }\n\n  return (\n    <HoverCard>\n      <HoverCardTrigger>\n        <a\n          ref={setEl}\n          href={href}\n          target=\"_blank\"\n          rel=\"noreferrer\"\n          data-citation=\"\"\n          data-lit={lit ? \"\" : undefined}\n          aria-label={name}\n          aria-describedby={describedBy}\n          onPointerEnter={() => store.point(\"hovered\", id, \"mark\")}\n          onPointerLeave={() => store.release(\"hovered\", id, \"mark\")}\n          onFocus={() => store.point(\"focused\", id, \"mark\")}\n          onBlur={() => store.release(\"focused\", id, \"mark\")}\n          className={\"ms-1 align-baseline \" + look + \" \" + MARK_LINK + \" \" + markInk(lit) + \" \" + MARK_HIT + \" \" + FOCUS}\n        >\n          {n}\n          <span id={describedBy} className=\"sr-only\">\n            {site ? `${site}, opens in a new tab` : \"Opens in a new tab\"}\n          </span>\n        </a>\n      </HoverCardTrigger>\n      <HoverCardContent size=\"md\">\n        <SourcePreview source={source} quote={quote} />\n      </HoverCardContent>\n    </HoverCard>\n  );\n}\n\n/** The card: the kind's icon and the site, the title as the link, the passage in quotes, when it was read. */\nfunction SourcePreview({ source, quote }: { source: ParsedSource; quote?: string }) {\n  const passage = quote ?? source.passage;\n  const titleLook = \"line-clamp-2 text-litt-small text-litt-fg \" + (source.kind === \"file\" ? \"font-litt-mono font-normal \" : \"font-litt-title \");\n  return (\n    <div className=\"grid gap-1\">\n      {source.site && (\n        <p className=\"flex items-center gap-2 text-litt-small text-litt-fg-muted\">\n          <KindIcon kind={source.kind} className=\"text-litt-fg-subtle\" />\n          <span className=\"min-w-0 truncate\">{source.site}</span>\n        </p>\n      )}\n      {source.href ? (\n        <a href={source.href} target=\"_blank\" rel=\"noreferrer\" className={titleLook + \"rounded-litt-control hover:underline \" + FOCUS}>\n          {source.title}\n        </a>\n      ) : (\n        <p className={titleLook}>{source.title}</p>\n      )}\n      {passage && <p className=\"line-clamp-4 text-litt-small text-litt-fg-muted\">{`“${passage}”`}</p>}\n      {source.readAt && <p className=\"mt-1 text-litt-caption font-normal tabular-nums text-litt-fg-subtle\">Read {source.readAt}</p>}\n    </div>\n  );\n}\n\n/* ----------------------------------------------------------------- SourceList */\n\nexport type SourceListProps = {\n  /** Sources to list in place of the turn's, such as one search's results. A result the turn cites wears its number; the others show their icon and say \"Not cited\". */\n  sources?: Source[];\n  /** The list's name: its heading, with the count, and its name for assistive tech. */\n  label?: string;\n  /** Keeps the label for assistive tech only, as inside a search call whose row already says what the list is. */\n  hideLabel?: boolean;\n  /** Extra classes for the list, such as a top margin. */\n  className?: string;\n};\n\n/**\n * The sources as rows: the turn's under the answer, or one search's results inside its ToolCall's details. Each row is a\n * link that opens the source in a new tab, and resting on it lights the marks that cite it.\n */\nexport function SourceList({ sources, label = \"Sources\", hideLabel = false, className = \"\" }: SourceListProps) {\n  const ctx = useContext(SourcesContext);\n  const labelId = useId();\n  const own = useMemo(() => (sources ? parseSources(sources) : null), [sources]);\n  if (!ctx) {\n    warnOnce(\"list-outside\", \"litt: <SourceList> sits outside a Sources and draws nothing.\");\n    return null;\n  }\n  const rows = own ?? ctx.list;\n  if (!rows.length) return null;\n\n  return (\n    <div\n      className={\n        \"grid min-w-0 font-litt-sans whitespace-normal in-[[data-material=flat]]:rounded-litt-surface in-[[data-material=flat]]:bg-litt-surface-3 in-[[data-material=flat]]:p-2 \" +\n        \"in-[[data-material=flat]]:forced-colors:border \" +\n        className\n      }\n    >\n      <p id={labelId} className={hideLabel ? \"sr-only\" : \"flex min-h-8 items-center gap-2 in-[[data-material=flat]]:px-2\"}>\n        <span className=\"text-litt-small font-litt-title text-litt-fg\">{label}</span>\n        <span className=\"text-litt-caption font-normal tabular-nums text-litt-fg-subtle\">{rows.length}</span>\n      </p>\n      <ol role=\"list\" aria-labelledby={labelId} className=\"m-0 grid list-none p-0\">\n        {rows.map((row) => (\n          <SourceRow key={row.id} row={row} n={own ? (ctx.byId.get(row.id)?.n ?? null) : row.n} store={ctx.store} />\n        ))}\n      </ol>\n    </div>\n  );\n}\n\n/* The fill hangs 8px into the gutter on the start side (as MessageActions do), so the lead sits on the turn's edge where a\n   Tool call row shows its mark, and the row never runs past its container's end. */\nconst ROW =\n  \"-ms-2 grid min-h-11 grid-cols-[1rem_minmax(0,1fr)] items-start gap-x-2 rounded-litt-control px-2 py-1 text-start \" +\n  \"in-[[data-material=flat]]:ms-0 in-[[data-material=flat]]:rounded-[calc(var(--litt-radius-surface)-4px)] \" +\n  \"transition-colors duration-(--litt-dur-instant) ease-litt\";\nconst ROW_LINK =\n  \"hover:bg-[color-mix(in_oklab,var(--litt-fg)_6%,transparent)] pointer-coarse:active:bg-[color-mix(in_oklab,var(--litt-fg)_6%,transparent)] \" +\n  \"focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-litt-focus [-webkit-tap-highlight-color:transparent]\";\nconst ROW_LIT = \"bg-[color-mix(in_oklab,var(--litt-fg)_6%,transparent)] forced-colors:bg-[Highlight] forced-colors:text-[HighlightText]\";\n\n/** One row: the lead (the number chip, or the kind's icon for an uncited result), the title over the site and the read words. */\nfunction SourceRow({ row, n, store }: { row: ParsedSource; n: number | null; store: LightStore }) {\n  const lit = useLit(store, row.id);\n  const meta = [row.site, row.readAt ? `Read ${row.readAt}` : null].filter(Boolean).join(\" · \");\n  const body = (\n    <>\n      <span className=\"flex h-5 items-center\" aria-hidden={n === null ? undefined : \"true\"}>\n        {n === null ? (\n          <>\n            <KindIcon kind={row.kind} className=\"text-litt-fg-subtle\" />\n            <span className=\"sr-only\">Not cited.</span>\n          </>\n        ) : (\n          <span className={chip(lit)}>{n}</span>\n        )}\n      </span>\n      <span className=\"grid min-w-0\">\n        <span className={\"truncate text-litt-small text-litt-fg \" + (row.kind === \"file\" ? \"font-litt-mono\" : \"\")}>\n          {n !== null && <span className=\"sr-only\">Source {n}: </span>}\n          {row.title}\n        </span>\n        {meta && <span className=\"truncate text-litt-caption font-normal tabular-nums text-litt-fg-subtle\">{meta}</span>}\n      </span>\n    </>\n  );\n  const look = ROW + (lit ? \" \" + ROW_LIT : \"\");\n  return (\n    <li className=\"min-w-0\">\n      {row.href ? (\n        <a\n          href={row.href}\n          target=\"_blank\"\n          rel=\"noreferrer\"\n          data-lit={lit ? \"\" : undefined}\n          onPointerEnter={() => store.point(\"hovered\", row.id, \"row\")}\n          onPointerLeave={() => store.release(\"hovered\", row.id, \"row\")}\n          onFocus={() => store.point(\"focused\", row.id, \"row\")}\n          onBlur={() => store.release(\"focused\", row.id, \"row\")}\n          className={look + \" \" + ROW_LINK}\n        >\n          {body}\n          <span className=\"sr-only\">Opens in a new tab.</span>\n        </a>\n      ) : (\n        <div data-lit={lit ? \"\" : undefined} className={look}>\n          {body}\n        </div>\n      )}\n    </li>\n  );\n}\n",
      "target": "components/litt/sources.tsx"
    }
  ],
  "docs": "Import Sources, Citation and SourceList from components/litt/sources. Wrap the turn's parts in Sources with the sources ({ id, title, url, site?, passage?, readAt?, kind? }); a source's number is its place in that list. Have your markdown renderer turn a #cite-<id> link into <Citation source=\"<id>\" /> (citationId reads the href, unlinkCitations cleans text for Copy). Put a SourceList after the answer, and a SourceList with the search's own sources in a ToolCall's details.",
  "meta": {
    "version": "2026.11.06",
    "released": "2026-11-06",
    "changelog": "https://littmd.vercel.app/changelog.md"
  }
}
