{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"sources","type":"registry:ui","title":"Sources","description":"The citations under an answer: a pill carrying a stack of domain monograms and a count, expanding into scannable source cards with title, host, and the line worth reading","categories":["ai-chat"],"registryDependencies":["https://fancy-ui.rama.app/r/internals.json","https://fancy-ui.rama.app/r/internals-dom.json","https://fancy-ui.rama.app/r/sound.json","https://fancy-ui.rama.app/r/utils.json"],"files":[{"path":"react/src/components/sources/SourceCard.tsx","content":"\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport { cn } from \"../../utils\";\nimport type { SourceData } from \"../../internals/ai-types\";\nimport { hostOf, monogram } from \"../../internals/host\";\nimport { sanitizeHref } from \"../../internals/markdown\";\nimport \"./source-card.css\";\n\n/**\n * Props for SourceCard\n */\nexport interface SourceCardProps {\n\t/** The document being cited. Only `title` is ever guaranteed to render. */\n\tsource: SourceData;\n\t/** Replaces the monogram: a favicon you host, a logo, an icon. */\n\ticon?: ReactNode;\n\t/**\n\t * Renders the plain, non-anchor shape even when the source has a url. For a\n\t * card embedded in a surface — a tooltip preview — that cannot host a link\n\t * reachable by keyboard.\n\t */\n\tinteractive?: boolean;\n\t/** Additional CSS classes */\n\tclassName?: string;\n}\n\n/**\n * A model-provided url made safe for an anchor's `href`, or `null` when it\n * must not render as a link at all: a disallowed scheme is rejected outright\n * by the shared markdown-link sanitizer, and a scheme-less host is promoted\n * to `https://` first. A genuine relative path (\"/local/guide\") has nowhere\n * else to resolve against and is left alone.\n */\nfunction resolveHref(raw: string): string | null {\n\tif (raw === \"\") return null;\n\tconst hasScheme = /^[a-z][a-z0-9+.-]*:/i.test(raw) || raw.startsWith(\"//\");\n\tconst host = raw.split(/[/?#]/, 1)[0] as string;\n\tconst looksHostLike = !hasScheme && !raw.startsWith(\"/\") && host.includes(\".\");\n\treturn sanitizeHref(looksHostLike ? `https://${raw}` : raw);\n}\n\n/** One citation: monogram, title, host, and the line worth reading. */\nexport function SourceCard({ source, icon, interactive = true, className }: SourceCardProps) {\n\t// An explicit `domain` always wins — a caller who says \"the standards body\"\n\t// means that, not `www.w3.org`.\n\tconst domain = source.domain || hostOf(source.url);\n\t// The host names the place, so it is what the circle stands for; a source\n\t// with no parsable host falls back to its own title.\n\tconst mark = monogram(domain || source.title);\n\t// A citation with nowhere to go is still worth showing — it just must not\n\t// pretend to be a link. A model-provided url also has to clear the same\n\t// scheme check every link in this family runs through, and a bare host\n\t// (\"docs.example.dev/guide\") has to become absolute before it goes in an\n\t// `href`, or the browser resolves it against this app's own origin instead\n\t// of the cited site.\n\tconst href = interactive && typeof source.url === \"string\" ? (resolveHref(source.url) ?? \"\") : \"\";\n\n\tconst body = (\n\t\t<>\n\t\t\t<span className=\"ft-source-mark flex-none\" aria-hidden=\"true\">\n\t\t\t\t{/*\n\t\t\t\t\tTruthiness, not nullishness: the Svelte source branches on\n\t\t\t\t\t`{#if icon}`, so a caller writing `icon={cond && <Logo />}`\n\t\t\t\t\twith a false `cond` gets the monogram back rather than an\n\t\t\t\t\tempty circle.\n\t\t\t\t*/}\n\t\t\t\t{icon || mark}\n\t\t\t</span>\n\n\t\t\t<span className=\"flex min-w-0 flex-col gap-0.5\">\n\t\t\t\t<span className=\"ft-source-title text-foreground text-xs leading-snug font-medium\">\n\t\t\t\t\t{source.title}\n\t\t\t\t</span>\n\t\t\t\t{domain && (\n\t\t\t\t\t<span className=\"ft-source-domain text-muted-foreground text-[0.6875rem] leading-none\">\n\t\t\t\t\t\t{domain}\n\t\t\t\t\t</span>\n\t\t\t\t)}\n\t\t\t\t{source.snippet && (\n\t\t\t\t\t<span className=\"ft-source-snippet text-muted-foreground mt-0.5 text-[0.6875rem] leading-snug\">\n\t\t\t\t\t\t{source.snippet}\n\t\t\t\t\t</span>\n\t\t\t\t)}\n\t\t\t</span>\n\t\t</>\n\t);\n\n\tif (href) {\n\t\treturn (\n\t\t\t// `nofollow ugc` because a model chose this link, not the author of the\n\t\t\t// page it sits on; `noopener noreferrer` because the tab it opens has no\n\t\t\t// business reaching back into the app or announcing where the reader\n\t\t\t// came from.\n\t\t\t<a\n\t\t\t\thref={href}\n\t\t\t\ttarget=\"_blank\"\n\t\t\t\trel=\"noopener noreferrer nofollow ugc\"\n\t\t\t\tclassName={cn(\"ft-source-card ft-source-linked\", className)}\n\t\t\t>\n\t\t\t\t{body}\n\t\t\t</a>\n\t\t);\n\t}\n\n\treturn <div className={cn(\"ft-source-card\", className)}>{body}</div>;\n}\n","type":"registry:ui","target":"@ui/fancy/components/sources/SourceCard.tsx"},{"path":"react/src/components/sources/Sources.tsx","content":"\"use client\";\n\nimport { forwardRef, useMemo, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"../../utils\";\nimport type { SourceData } from \"../../internals/ai-types\";\nimport { useFancyId } from \"../../internals/use-id\";\nimport { SourcesTrigger } from \"./SourcesTrigger\";\nimport { SourcesList } from \"./SourcesList\";\nimport { SOURCES_CONTEXT_KEY } from \"./types\";\nimport type { SourcesContext } from \"./types\";\nimport { useSoundCue } from \"../../sound/use-sound\";\n\n/**\n * Props for Sources\n */\nexport interface SourcesProps {\n\t/** The documents backing the answer, in the order they should be read. */\n\tsources: SourceData[];\n\t/** Whether the list is expanded. Starts closed. */\n\topen?: boolean;\n\t/** Called whenever the list opens or closes. */\n\tonToggle?: (open: boolean) => void;\n\t/** Replaces the default trigger-and-list composition entirely. */\n\tchildren?: ReactNode;\n\t/** Additional CSS classes */\n\tclassName?: string;\n\t/**\n\t * Plays open/close through the sound controller when the source list\n\t * expands or collapses. Off by default; only audible once the user\n\t * has enabled sound.\n\t */\n\tsound?: boolean;\n}\n\n/**\n * The citations under an answer: a pill you can ignore and a set of cards you\n * can scan.\n *\n * The root element arrives through the ref channel rather than a `ref` prop,\n * per PORTING.md — the Svelte source declares `ref = $bindable(null)`.\n */\nexport const Sources = forwardRef<HTMLDivElement, SourcesProps>(function Sources(\n\t{ sources, open: openProp, onToggle, children, className, sound = false },\n\tref\n) {\n\t// The Svelte source's `open` is `$bindable(false)`: a consumer can bind it,\n\t// or leave it alone and let the component keep writing its own copy. React\n\t// has no such channel, so the prop is controlled when it is passed and this\n\t// local copy takes over when it is not. Either way `onToggle` fires with the\n\t// same value.\n\tconst [uncontrolledOpen, setUncontrolledOpen] = useState(false);\n\tconst isControlled = openProp !== undefined;\n\tconst open = isControlled ? openProp : uncontrolledOpen;\n\n\tconst uid = useFancyId();\n\tconst listId = `${uid}-list`;\n\n\tconst playCue = useSoundCue(sound);\n\n\t// Rebuilt only when something a part reads actually changes — that rebuild is\n\t// what re-renders the trigger and the list below, and it is the React\n\t// counterpart of the Svelte context's live getters.\n\tconst context = useMemo<SourcesContext>(\n\t\t() => ({\n\t\t\topen: { current: open },\n\t\t\tcount: sources.length,\n\t\t\tsources,\n\t\t\tlistId,\n\t\t\ttoggle() {\n\t\t\t\tconst next = !open;\n\t\t\t\tif (!isControlled) setUncontrolledOpen(next);\n\t\t\t\t// The cue sits on the one mutation point, between the state\n\t\t\t\t// write and the callback: a controlled `open` written from\n\t\t\t\t// outside never comes through here, so it plays nothing.\n\t\t\t\tplayCue(next ? \"open\" : \"close\");\n\t\t\t\tonToggle?.(next);\n\t\t\t},\n\t\t}),\n\t\t[open, sources, listId, isControlled, onToggle, playCue]\n\t);\n\n\treturn (\n\t\t<SOURCES_CONTEXT_KEY.Provider value={context}>\n\t\t\t<div ref={ref} className={cn(\"ft-sources w-full\", className)} data-open={open}>\n\t\t\t\t{/*\n\t\t\t\t\tTruthiness, not nullishness: the Svelte source branches on\n\t\t\t\t\t`{#if children}`, so `{cond && <Parts />}` with a false\n\t\t\t\t\t`cond` falls through to the default composition rather than\n\t\t\t\t\temptying the root.\n\t\t\t\t*/}\n\t\t\t\t{children || (\n\t\t\t\t\t<>\n\t\t\t\t\t\t<SourcesTrigger />\n\t\t\t\t\t\t<SourcesList />\n\t\t\t\t\t</>\n\t\t\t\t)}\n\t\t\t</div>\n\t\t</SOURCES_CONTEXT_KEY.Provider>\n\t);\n});\n","type":"registry:ui","target":"@ui/fancy/components/sources/Sources.tsx"},{"path":"react/src/components/sources/SourcesList.tsx","content":"\"use client\";\n\nimport { useContext } from \"react\";\nimport type { CSSProperties, ReactNode } from \"react\";\nimport { cn } from \"../../utils\";\nimport { useInertAttribute } from \"../../internals/dom/use-inert-attribute\";\nimport type { SourceData } from \"../../internals/ai-types\";\nimport { SourceCard } from \"./SourceCard\";\nimport { SOURCES_CONTEXT_KEY } from \"./types\";\nimport \"./sources-list.css\";\n\n/**\n * Props for SourcesList\n */\nexport interface SourcesListProps {\n\t/** Replaces the default card. Receives the source and its index. */\n\titem?: (source: SourceData, index: number) => ReactNode;\n\t/** Additional CSS classes */\n\tclassName?: string;\n}\n\n/** The expanding region of cards. */\nexport function SourcesList({ item, className }: SourcesListProps) {\n\t// Undefined when the list is used outside a Sources root: it then renders an\n\t// empty, permanently open region rather than throwing.\n\tconst sources = useContext(SOURCES_CONTEXT_KEY);\n\n\tconst items = sources?.sources ?? [];\n\tconst isOpen = sources?.open.current ?? true;\n\tconst inertRef = useInertAttribute<HTMLUListElement>(!isOpen);\n\n\treturn (\n\t\t<div className={cn(\"ft-sources-list\", isOpen && \"ft-open\")}>\n\t\t\t<div className=\"overflow-hidden\">\n\t\t\t\t{/*\n\t\t\t\t\t`inert` is written straight to the node by `useInertAttribute`, never\n\t\t\t\t\tas a JSX prop: `inert={true}` is dropped by React 18 and `inert=\"\"` is\n\t\t\t\t\trejected by React 19, so no single prop spelling covers this package's\n\t\t\t\t\tpeer range. The hook emits the same attribute on both.\n\t\t\t\t*/}\n\t\t\t\t<ul\n\t\t\t\t\tref={inertRef}\n\t\t\t\t\tid={sources?.listId}\n\t\t\t\t\taria-label=\"Sources\"\n\t\t\t\t\tclassName={cn(\"ft-sources-grid\", className)}\n\t\t\t\t>\n\t\t\t\t\t{items.map((source, index) => (\n\t\t\t\t\t\t/*\n\t\t\t\t\t\t\tThe entrance animation hangs off `.ft-in` rather than mount, because\n\t\t\t\t\t\t\tthe cards never unmount: collapsing is a height transition over live\n\t\t\t\t\t\t\tDOM, so re-adding the class is the only thing that can replay it.\n\t\t\t\t\t\t*/\n\t\t\t\t\t\t<li\n\t\t\t\t\t\t\tkey={source.id}\n\t\t\t\t\t\t\tclassName={cn(\"ft-sources-item\", isOpen && \"ft-in\")}\n\t\t\t\t\t\t\tstyle={{ \"--ft-sources-index\": index } as CSSProperties}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{item ? item(source, index) : <SourceCard source={source} />}\n\t\t\t\t\t\t</li>\n\t\t\t\t\t))}\n\t\t\t\t</ul>\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n","type":"registry:ui","target":"@ui/fancy/components/sources/SourcesList.tsx"},{"path":"react/src/components/sources/SourcesTrigger.tsx","content":"\"use client\";\n\nimport { useContext } from \"react\";\nimport type { ComponentPropsWithoutRef, MouseEvent } from \"react\";\nimport { cn } from \"../../utils\";\nimport { hostOf, monogram } from \"../../internals/host\";\nimport { SOURCES_CONTEXT_KEY } from \"./types\";\nimport \"./sources-trigger.css\";\n\n/**\n * Props for SourcesTrigger\n */\ninterface SourcesTriggerOwnProps {\n\t/** Overrides the count line. Defaults to \"4 sources\", singular at one. */\n\tlabel?: string;\n\t/** Additional CSS classes */\n\tclassName?: string;\n}\n\n/**\n * Native `<button>` attributes and listeners are forwarded to the trigger; the managed\n * ARIA, state and structural attributes stay the component's.\n */\nexport interface SourcesTriggerProps\n\textends\n\t\tOmit<ComponentPropsWithoutRef<\"button\">, keyof SourcesTriggerOwnProps>,\n\t\tSourcesTriggerOwnProps {}\n\n/** How many monograms fit in the stack before it stops meaning anything. */\nconst STACK_LIMIT = 3;\n\n/** The collapsed pill: monogram stack, count, chevron. */\nexport function SourcesTrigger({ label, className, onClick, ...rest }: SourcesTriggerProps) {\n\t// Undefined when the pill is used outside a Sources root: it then renders an\n\t// empty, inert count rather than throwing.\n\tconst sources = useContext(SOURCES_CONTEXT_KEY);\n\n\tconst count = sources?.count ?? 0;\n\tconst isOpen = sources?.open.current ?? false;\n\tconst stack = (sources?.sources ?? []).slice(0, STACK_LIMIT);\n\tconst text = label ?? `${count} ${count === 1 ? \"source\" : \"sources\"}`;\n\n\treturn (\n\t\t<button\n\t\t\t{...rest}\n\t\t\ttype=\"button\"\n\t\t\tclassName={cn(\n\t\t\t\t\"ft-sources-trigger text-muted-foreground hover:text-foreground inline-flex items-center gap-2 rounded-full px-2 py-1 text-xs\",\n\t\t\t\tclassName\n\t\t\t)}\n\t\t\taria-expanded={isOpen}\n\t\t\taria-controls={sources?.listId}\n\t\t\tonClick={(event: MouseEvent<HTMLButtonElement>) => {\n\t\t\t\tsources?.toggle();\n\t\t\t\tonClick?.(event);\n\t\t\t}}\n\t\t>\n\t\t\t{stack.length > 0 && (\n\t\t\t\t// Decorative: every name in it is spelled out in the list below.\n\t\t\t\t<span className=\"ft-sources-stack flex flex-none items-center\" aria-hidden=\"true\">\n\t\t\t\t\t{stack.map((source) => (\n\t\t\t\t\t\t<span key={source.id} className=\"ft-sources-chip\">\n\t\t\t\t\t\t\t{monogram(source.domain || hostOf(source.url) || source.title)}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t))}\n\t\t\t\t</span>\n\t\t\t)}\n\n\t\t\t<span className=\"font-medium\">{text}</span>\n\n\t\t\t<svg\n\t\t\t\tclassName={cn(\"ft-sources-chevron size-3 flex-none\", isOpen && \"ft-open\")}\n\t\t\t\tviewBox=\"0 0 24 24\"\n\t\t\t\tfill=\"none\"\n\t\t\t\tstroke=\"currentColor\"\n\t\t\t\tstrokeWidth=\"2.5\"\n\t\t\t\tstrokeLinecap=\"round\"\n\t\t\t\tstrokeLinejoin=\"round\"\n\t\t\t\taria-hidden=\"true\"\n\t\t\t>\n\t\t\t\t<path d=\"m9 6 6 6-6 6\" />\n\t\t\t</svg>\n\t\t</button>\n\t);\n}\n","type":"registry:ui","target":"@ui/fancy/components/sources/SourcesTrigger.tsx"},{"path":"react/src/components/sources/index.ts","content":"\"use client\";\n\nexport { Sources } from \"./Sources\";\nexport type { SourcesProps } from \"./Sources\";\nexport { SourcesTrigger } from \"./SourcesTrigger\";\nexport type { SourcesTriggerProps } from \"./SourcesTrigger\";\nexport { SourcesList } from \"./SourcesList\";\nexport type { SourcesListProps } from \"./SourcesList\";\nexport { SourceCard } from \"./SourceCard\";\nexport type { SourceCardProps } from \"./SourceCard\";\nexport { SOURCES_CONTEXT_KEY } from \"./types\";\nexport type { SourcesContext } from \"./types\";\n","type":"registry:ui","target":"@ui/fancy/components/sources/index.ts"},{"path":"react/src/components/sources/source-card.css","content":".ft-source-card {\n\tdisplay: flex;\n\tgap: 0.5rem;\n\tborder-radius: 0.5rem;\n\tborder: 1px solid\n\t\tvar(\n\t\t\t--ft-sources-card-border,\n\t\t\tvar(--color-border, color-mix(in oklab, currentColor 15%, transparent))\n\t\t);\n\tbackground: var(--ft-sources-card-bg, color-mix(in oklab, currentColor 3%, transparent));\n\tpadding: 0.5rem 0.625rem;\n\ttext-decoration: none;\n\tcolor: inherit;\n}\n\n.ft-source-linked:hover,\n.ft-source-linked:focus-visible {\n\tbackground: var(--ft-sources-card-hover-bg, color-mix(in oklab, currentColor 7%, transparent));\n}\n\n/*\n * The monogram stands in for a favicon: no request leaves the reader's browser\n * to the cited site, and there is no broken image to fall back from.\n */\n.ft-source-mark {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tjustify-content: center;\n\twidth: 1.25rem;\n\theight: 1.25rem;\n\tborder-radius: 9999px;\n\tbackground: var(--ft-sources-chip-bg, color-mix(in oklab, currentColor 10%, transparent));\n\tcolor: var(--ft-sources-chip-fg, inherit);\n\tfont-size: 0.625rem;\n\tfont-weight: 600;\n\tline-height: 1;\n}\n\n/*\n * Two lines for a title, two for the snippet: enough to tell two results\n * apart, few enough that a row of cards stays a row.\n */\n.ft-source-title,\n.ft-source-snippet {\n\tdisplay: -webkit-box;\n\t-webkit-box-orient: vertical;\n\t-webkit-line-clamp: 2;\n\tline-clamp: 2;\n\toverflow: hidden;\n}\n\n/* A host is an identifier, so it is set like one — and never wrapped. */\n.ft-source-domain {\n\toverflow: hidden;\n\tfont-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n\ttext-overflow: ellipsis;\n\twhite-space: nowrap;\n}\n\n@media (prefers-reduced-motion: no-preference) {\n\t.ft-source-linked {\n\t\ttransition: background-color 150ms ease;\n\t}\n}\n","type":"registry:ui","target":"@ui/fancy/components/sources/source-card.css"},{"path":"react/src/components/sources/sources-list.css","content":"/*\n * 0fr → 1fr on a one-row grid is the only way to transition \"auto\" height\n * without measuring anything. The inner wrapper does the clipping.\n */\n.ft-sources-list {\n\tdisplay: grid;\n\tgrid-template-rows: 0fr;\n}\n\n.ft-sources-list.ft-open {\n\tgrid-template-rows: 1fr;\n}\n\n/*\n * Cards flow into as many columns as the container can hold, so the same list\n * is a stack in a chat column and a grid in a document.\n */\n.ft-sources-grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fill, minmax(var(--ft-sources-min-column, 13rem), 1fr));\n\tgap: var(--ft-sources-gap, 0.5rem);\n\t/* The list reset is written out rather than borrowed from a utility, so the\n\t   compound looks the same in an app whose CSS never resets `ul`. */\n\tmargin: 0;\n\tpadding: 0.5rem 0 0;\n\tlist-style: none;\n}\n\n.ft-sources-item {\n\tdisplay: flex;\n}\n\n.ft-sources-item > * {\n\tflex: 1;\n\tmin-width: 0;\n}\n\n/*\n * Everything that moves lives behind the query, so reduced motion gets the\n * same two states with nothing to shorten or fall back from.\n */\n@media (prefers-reduced-motion: no-preference) {\n\t.ft-sources-list {\n\t\ttransition: grid-template-rows 260ms cubic-bezier(0.4, 0, 0.2, 1);\n\t}\n\n\t.ft-sources-item.ft-in {\n\t\tanimation: ft-sources-in 240ms cubic-bezier(0.2, 0, 0, 1) backwards;\n\t\t/* Clamped: past a handful of cards the stagger stops reading as sequence\n\t\t   and starts reading as lag, so the tail all lands together. */\n\t\tanimation-delay: calc(min(var(--ft-sources-index, 0), 6) * 45ms);\n\t}\n}\n\n@keyframes ft-sources-in {\n\tfrom {\n\t\topacity: 0;\n\t\ttransform: translateY(0.375rem);\n\t}\n\tto {\n\t\topacity: 1;\n\t\ttransform: none;\n\t}\n}\n","type":"registry:ui","target":"@ui/fancy/components/sources/sources-list.css"},{"path":"react/src/components/sources/sources-trigger.css","content":".ft-sources-trigger {\n\tborder: 1px solid\n\t\tvar(\n\t\t\t--ft-sources-card-border,\n\t\t\tvar(--color-border, color-mix(in oklab, currentColor 15%, transparent))\n\t\t);\n\tbackground: var(--ft-sources-trigger-bg, transparent);\n}\n\n.ft-sources-trigger:hover,\n.ft-sources-trigger:focus-visible {\n\tbackground: var(--ft-sources-card-hover-bg, color-mix(in oklab, currentColor 7%, transparent));\n}\n\n/*\n * Overlapping monograms read as \"a handful of places\", which is the whole job\n * of the collapsed pill. The ring is the surface colour rather than a border,\n * so the circles cut into each other instead of stacking two outlines.\n */\n.ft-sources-chip {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tjustify-content: center;\n\twidth: 1rem;\n\theight: 1rem;\n\tborder-radius: 9999px;\n\tbox-shadow: 0 0 0 1.5px var(--ft-sources-chip-ring, var(--color-background, canvas));\n\tbackground: var(--ft-sources-chip-bg, color-mix(in oklab, currentColor 12%, transparent));\n\tcolor: var(--ft-sources-chip-fg, inherit);\n\tfont-size: 0.5rem;\n\tfont-weight: 600;\n\tline-height: 1;\n}\n\n.ft-sources-chip:not(:first-child) {\n\tmargin-inline-start: -0.3125rem;\n}\n\n.ft-sources-chevron.ft-open {\n\ttransform: rotate(90deg);\n}\n\n@media (prefers-reduced-motion: no-preference) {\n\t.ft-sources-trigger {\n\t\ttransition:\n\t\t\tbackground-color 150ms ease,\n\t\t\tcolor 150ms ease;\n\t}\n\n\t.ft-sources-chevron {\n\t\ttransition: transform 220ms cubic-bezier(0.4, 0, 0.2, 1);\n\t}\n}\n","type":"registry:ui","target":"@ui/fancy/components/sources/sources-trigger.css"},{"path":"react/src/components/sources/types.ts","content":"\"use client\";\n\n/**\n * The contract between the sources root and its parts.\n *\n * `Sources` publishes a live, read-only view of the citation set it holds, plus\n * the one function that changes anything about it. Every part reads it through\n * context rather than having the same values threaded back down as props, so a\n * trigger and a list can sit at different depths of a consumer's own markup and\n * still agree on what is open and what is being cited.\n */\n\nimport { createContext } from \"react\";\nimport type { SourceData } from \"../../internals/ai-types\";\n\n/** What the root publishes. Parts read it; only the root writes it. */\nexport interface SourcesContext {\n\t/**\n\t * Whether the list is expanded. The Svelte side wraps it in a box so a\n\t * reactive read tracks the root's prop; the box is kept here because it is\n\t * part of the published contract a consumer writes its own parts against —\n\t * what makes a part re-render is the root rebuilding this object when the\n\t * state changes.\n\t */\n\treadonly open: { readonly current: boolean };\n\t/** How many sources back the answer. */\n\treadonly count: number;\n\t/** The sources themselves, so a list never needs them threaded down as a prop. */\n\treadonly sources: readonly SourceData[];\n\t/** The id the list puts on its region, so a trigger can point `aria-controls` at it. */\n\treadonly listId: string;\n\t/** Flip the list open or closed. */\n\ttoggle(): void;\n}\n\n/**\n * The context a part reads to find the root above it. The Svelte source\n * publishes it under a `Symbol` context key; React's own context object plays\n * that role here, so the exported name is kept and the value is a\n * `React.Context` rather than a symbol:\n *\n * ```tsx\n * const sources = useContext(SOURCES_CONTEXT_KEY);\n * ```\n *\n * Read it as optional. Every shipped part does, so a part rendered outside a\n * root degrades instead of throwing.\n */\nexport const SOURCES_CONTEXT_KEY = createContext<SourcesContext | undefined>(undefined);\nSOURCES_CONTEXT_KEY.displayName = \"SourcesContext\";\n","type":"registry:ui","target":"@ui/fancy/components/sources/types.ts"}]}