{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sources",
  "title": "AI Sources",
  "description": "Collapsible citations viewer with favicons, source links, and snippets for RAG/search results display.",
  "dependencies": [
    "@radix-ui/react-collapsible",
    "lucide-react"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/blocks/ai/ai-sources/components/elements/ai-sources.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport * as CollapsiblePrimitive from \"@radix-ui/react-collapsible\";\nimport { ChevronDown, ExternalLink, FileText } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\ninterface Source {\n  url: string;\n  title: string;\n  favicon?: string;\n  snippet?: string;\n}\n\ninterface AiSourcesContextValue {\n  sources: Source[];\n}\n\nconst AiSourcesContext = React.createContext<AiSourcesContextValue | null>(\n  null,\n);\n\nfunction useSourcesContext() {\n  const context = React.useContext(AiSourcesContext);\n  if (!context) {\n    throw new Error(\"AiSources components must be used within <AiSources>\");\n  }\n  return context;\n}\n\ninterface AiSourcesProps {\n  sources?: Source[];\n  defaultOpen?: boolean;\n  open?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  children?: React.ReactNode;\n  className?: string;\n}\n\nfunction AiSources({\n  sources = [],\n  defaultOpen = false,\n  open: controlledOpen,\n  onOpenChange,\n  children,\n  className,\n}: AiSourcesProps) {\n  const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen);\n\n  const isControlled = controlledOpen !== undefined;\n  const isOpen = isControlled ? controlledOpen : uncontrolledOpen;\n\n  const handleOpenChange = React.useCallback(\n    (open: boolean) => {\n      if (!isControlled) {\n        setUncontrolledOpen(open);\n      }\n      onOpenChange?.(open);\n    },\n    [isControlled, onOpenChange],\n  );\n\n  const contextValue = React.useMemo(() => ({ sources }), [sources]);\n\n  return (\n    <AiSourcesContext.Provider value={contextValue}>\n      <CollapsiblePrimitive.Root\n        data-slot=\"ai-sources\"\n        open={isOpen}\n        onOpenChange={handleOpenChange}\n        className={cn(\n          \"rounded-lg border border-border bg-card text-card-foreground overflow-hidden\",\n          className,\n        )}\n      >\n        {children}\n      </CollapsiblePrimitive.Root>\n    </AiSourcesContext.Provider>\n  );\n}\n\ninterface AiSourcesTriggerProps {\n  children?: React.ReactNode;\n  className?: string;\n}\n\nfunction AiSourcesTrigger({ children, className }: AiSourcesTriggerProps) {\n  const { sources } = useSourcesContext();\n\n  return (\n    <CollapsiblePrimitive.Trigger\n      data-slot=\"ai-sources-trigger\"\n      className={cn(\n        \"flex w-full items-center gap-3 px-4 py-3 text-sm font-medium transition-colors hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n        className,\n      )}\n    >\n      <div className=\"flex size-8 shrink-0 items-center justify-center rounded-md bg-muted\">\n        <FileText className=\"size-4 text-muted-foreground\" />\n      </div>\n      <div className=\"flex flex-1 items-center gap-2 text-left\">\n        {children || \"Sources\"}\n        <span className=\"inline-flex items-center justify-center rounded-full bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground\">\n          {sources.length}\n        </span>\n      </div>\n      <ChevronDown className=\"size-4 shrink-0 text-muted-foreground transition-transform duration-200 [[data-state=open]>&]:rotate-180\" />\n    </CollapsiblePrimitive.Trigger>\n  );\n}\n\ninterface AiSourcesContentProps {\n  children?: React.ReactNode;\n  className?: string;\n}\n\nfunction AiSourcesContent({ children, className }: AiSourcesContentProps) {\n  const { sources } = useSourcesContext();\n\n  return (\n    <CollapsiblePrimitive.Content\n      data-slot=\"ai-sources-content\"\n      className={cn(\n        \"border-t border-border data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down\",\n        className,\n      )}\n    >\n      <div className=\"p-4\">\n        {children || (\n          <div className=\"grid gap-2 sm:grid-cols-2\">\n            {sources.map((source, index) => (\n              <AiSource key={index} source={source} />\n            ))}\n          </div>\n        )}\n      </div>\n    </CollapsiblePrimitive.Content>\n  );\n}\n\ninterface AiSourceProps {\n  source: Source;\n  className?: string;\n}\n\nfunction AiSource({ source, className }: AiSourceProps) {\n  const [faviconError, setFaviconError] = React.useState(false);\n\n  const hostname = React.useMemo(() => {\n    try {\n      return new URL(source.url).hostname.replace(\"www.\", \"\");\n    } catch {\n      return source.url;\n    }\n  }, [source.url]);\n\n  const faviconUrl = React.useMemo(() => {\n    if (source.favicon) return source.favicon;\n    try {\n      const url = new URL(source.url);\n      return `https://www.google.com/s2/favicons?domain=${url.hostname}&sz=32`;\n    } catch {\n      return null;\n    }\n  }, [source.url, source.favicon]);\n\n  const handleFaviconError = React.useCallback(() => {\n    setFaviconError(true);\n  }, []);\n\n  const showFavicon = faviconUrl && !faviconError;\n\n  return (\n    <a\n      href={source.url}\n      target=\"_blank\"\n      rel=\"noopener noreferrer\"\n      data-slot=\"ai-source\"\n      className={cn(\n        \"group flex items-start gap-3 rounded-md border border-border bg-background p-3 transition-colors hover:bg-muted/50\",\n        className,\n      )}\n    >\n      <div className=\"flex size-8 shrink-0 items-center justify-center rounded-md bg-muted\">\n        {showFavicon ? (\n          <img\n            src={faviconUrl}\n            alt=\"\"\n            aria-hidden=\"true\"\n            className=\"size-4 rounded-sm\"\n            onError={handleFaviconError}\n          />\n        ) : (\n          <FileText className=\"size-4 text-muted-foreground\" />\n        )}\n      </div>\n      <div className=\"flex-1 min-w-0\">\n        <div className=\"flex items-center gap-1.5\">\n          <span className=\"font-medium text-sm truncate\">{source.title}</span>\n          <ExternalLink className=\"size-3 shrink-0 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100\" />\n        </div>\n        <span className=\"text-xs text-muted-foreground truncate block\">\n          {hostname}\n        </span>\n        {source.snippet && (\n          <p className=\"mt-1 text-xs text-muted-foreground line-clamp-2\">\n            {source.snippet}\n          </p>\n        )}\n      </div>\n    </a>\n  );\n}\n\ninterface AiSourcesListProps {\n  children?: React.ReactNode;\n  className?: string;\n}\n\nfunction AiSourcesList({ children, className }: AiSourcesListProps) {\n  return (\n    <div\n      data-slot=\"ai-sources-list\"\n      className={cn(\"grid gap-2 sm:grid-cols-2\", className)}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport {\n  AiSources,\n  AiSourcesTrigger,\n  AiSourcesContent,\n  AiSource,\n  AiSourcesList,\n};\nexport type { AiSourcesProps, Source };\n",
      "type": "registry:component"
    }
  ],
  "docs": "Compound component with trigger and content. Auto-fetches favicons from Google. Supports custom rendering.",
  "categories": [
    "ai"
  ],
  "type": "registry:ui"
}