{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ai-citations",
  "title": "AI Citations",
  "description": "Display citations and sources with collapsible source previews.",
  "registryDependencies": [
    "collapsible",
    "empty"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/ai/ai-citations.tsx",
      "content": "\"use client\";\n\nimport {\n  ChevronDown,\n  ExternalLink,\n  FileText,\n  Globe,\n  Quote,\n} from \"lucide-react\";\nimport { useCallback, useMemo, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  Collapsible,\n  CollapsibleContent,\n  CollapsibleTrigger,\n} from \"@/registry/new-york/ui/collapsible\";\nimport {\n  Empty,\n  EmptyDescription,\n  EmptyHeader,\n  EmptyMedia,\n  EmptyTitle,\n} from \"@/registry/new-york/ui/empty\";\n\nexport interface CitationSource {\n  id: string;\n  title: string;\n  url: string;\n  domain?: string;\n  snippet?: string;\n  author?: string;\n  publishedAt?: Date;\n  type?: \"web\" | \"document\" | \"paper\" | \"other\";\n}\n\nexport interface Citation {\n  id: string;\n  number: number;\n  sources: CitationSource[];\n  text?: string;\n}\n\nexport interface AICitationsProps {\n  citations: Citation[];\n  className?: string;\n  defaultExpanded?: boolean;\n  showInlineNumbers?: boolean;\n  onSourceClick?: (source: CitationSource) => void;\n}\n\nconst SOURCE_ICONS: Record<\n  NonNullable<CitationSource[\"type\"]>,\n  React.ComponentType<{ className?: string }>\n> = {\n  document: FileText,\n  paper: FileText,\n  web: Globe,\n  other: Globe,\n};\n\nfunction formatDomain(url: string): string {\n  try {\n    const urlObj = new URL(url);\n    return urlObj.hostname.replace(/^www\\./, \"\");\n  } catch {\n    return url;\n  }\n}\n\nfunction formatDate(date: Date): string {\n  const months = [\n    \"Jan\",\n    \"Feb\",\n    \"Mar\",\n    \"Apr\",\n    \"May\",\n    \"Jun\",\n    \"Jul\",\n    \"Aug\",\n    \"Sep\",\n    \"Oct\",\n    \"Nov\",\n    \"Dec\",\n  ];\n  const month = months[date.getMonth()];\n  const day = date.getDate();\n  const year = date.getFullYear();\n  return `${month} ${day}, ${year}`;\n}\n\ninterface CitationNumberProps {\n  number: number;\n  onClick?: () => void;\n  className?: string;\n  asButton?: boolean;\n  \"aria-label\"?: string;\n}\n\nfunction CitationNumber({\n  number,\n  onClick,\n  className,\n  asButton = true,\n  \"aria-label\": ariaLabel,\n}: CitationNumberProps) {\n  const baseClassName = cn(\n    \"inline-flex items-center justify-center rounded-full border bg-primary/10 font-mono font-semibold text-primary text-xs transition-colors\",\n    \"size-6 min-h-6 min-w-6 md:size-[24px] md:min-h-[24px] md:min-w-[24px]\",\n    asButton &&\n      onClick &&\n      \"cursor-pointer [-webkit-tap-highlight-color:transparent] hover:bg-primary/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 active:bg-primary/30\",\n    !asButton && \"pointer-events-none\",\n    className\n  );\n\n  const label = ariaLabel || `Citation ${number}`;\n\n  if (asButton && onClick) {\n    return (\n      <button\n        aria-label={label}\n        className={baseClassName}\n        onClick={onClick}\n        type=\"button\"\n      >\n        <span className=\"tabular-nums\">{number}</span>\n      </button>\n    );\n  }\n\n  return (\n    <span aria-label={label} className={baseClassName}>\n      <span className=\"tabular-nums\">{number}</span>\n    </span>\n  );\n}\n\ninterface SourceIconProps {\n  type?: CitationSource[\"type\"];\n  className?: string;\n}\n\nfunction SourceIcon({ type, className }: SourceIconProps) {\n  const IconComponent = SOURCE_ICONS[type || \"other\"] || SOURCE_ICONS.web;\n  return <IconComponent className={cn(\"size-4 text-primary\", className)} />;\n}\n\ninterface SourceMetadataProps {\n  source: CitationSource;\n}\n\nfunction SourceMetadata({ source }: SourceMetadataProps) {\n  const domain = source.domain || formatDomain(source.url);\n\n  return (\n    <div\n      aria-label=\"Source metadata\"\n      className=\"flex flex-wrap items-center gap-1 text-muted-foreground text-xs\"\n      role=\"group\"\n    >\n      <span className=\"truncate tabular-nums\">{domain}</span>\n      {source.author && (\n        <>\n          <span aria-hidden=\"true\" className=\"tabular-nums\">\n            •\n          </span>\n          <span className=\"tabular-nums\">{source.author}</span>\n        </>\n      )}\n      {source.publishedAt && (\n        <>\n          <span aria-hidden=\"true\" className=\"tabular-nums\">\n            •\n          </span>\n          <time\n            className=\"tabular-nums\"\n            dateTime={source.publishedAt.toISOString()}\n          >\n            {formatDate(source.publishedAt)}\n          </time>\n        </>\n      )}\n    </div>\n  );\n}\n\ninterface SourceSnippetProps {\n  snippet: string;\n}\n\nfunction SourceSnippet({ snippet }: SourceSnippetProps) {\n  return (\n    <div aria-label=\"Source excerpt\" className=\"flex gap-2 pl-10\" role=\"group\">\n      <Quote\n        aria-hidden=\"true\"\n        className=\"size-3 shrink-0 translate-y-0.5 text-muted-foreground\"\n      />\n      <p className=\"line-clamp-2 text-muted-foreground text-xs leading-relaxed\">\n        {snippet}\n      </p>\n    </div>\n  );\n}\n\ninterface SourcePreviewProps {\n  source: CitationSource;\n  citationNumber: number;\n  onSourceClick?: (source: CitationSource) => void;\n}\n\nfunction SourcePreview({\n  source,\n  citationNumber,\n  onSourceClick,\n}: SourcePreviewProps) {\n  const [isSnippetOpen, setIsSnippetOpen] = useState(false);\n  const handleClick = useCallback(() => {\n    onSourceClick?.(source);\n  }, [onSourceClick, source]);\n\n  return (\n    <Collapsible onOpenChange={setIsSnippetOpen} open={isSnippetOpen}>\n      <article\n        aria-labelledby={`source-title-${source.id}`}\n        className={cn(\n          \"group flex flex-col gap-2 rounded-md bg-muted/30 p-3 pl-4 transition-colors\",\n          \"focus-within:bg-muted/50 hover:bg-muted/50\",\n          \"border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1\",\n          \"touch-manipulation [-webkit-tap-highlight-color:transparent]\"\n        )}\n        onClick={handleClick}\n        role=\"article\"\n      >\n        <div className=\"flex flex-col items-center justify-between gap-3 sm:flex-row\">\n          <div className=\"flex min-w-0 flex-1 items-start gap-3\">\n            <div\n              aria-hidden=\"true\"\n              className=\"flex size-7 shrink-0 items-center justify-center rounded-md bg-background\"\n            >\n              <SourceIcon type={source.type} />\n            </div>\n            <div className=\"flex min-w-0 flex-1 flex-col gap-2\">\n              <div className=\"flex items-start gap-2\">\n                <h4\n                  className=\"line-clamp-2 font-medium text-sm leading-snug\"\n                  id={`source-title-${source.id}`}\n                >\n                  {source.title}\n                </h4>\n              </div>\n              <SourceMetadata source={source} />\n            </div>\n          </div>\n          <div className=\"flex shrink-0 items-center gap-1.5\">\n            {source.snippet && (\n              <CollapsibleTrigger\n                aria-label={isSnippetOpen ? \"Hide snippet\" : \"Show snippet\"}\n                className={cn(\n                  \"flex shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors\",\n                  \"min-h-[44px] min-w-[44px] sm:min-h-[24px] sm:min-w-[24px]\",\n                  \"hover:bg-background hover:text-foreground\",\n                  \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1\",\n                  \"[-webkit-tap-highlight-color:transparent] active:bg-background/80\"\n                )}\n                onClick={(e) => e.stopPropagation()}\n                type=\"button\"\n              >\n                <ChevronDown\n                  aria-hidden=\"true\"\n                  className={cn(\n                    \"size-3.5 transition-transform motion-safe:duration-200\",\n                    isSnippetOpen && \"rotate-180\"\n                  )}\n                />\n              </CollapsibleTrigger>\n            )}\n            <a\n              aria-label={`Open ${source.title} in new tab`}\n              className={cn(\n                \"flex shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors\",\n                \"min-h-[44px] min-w-[44px] sm:min-h-[24px] sm:min-w-[24px]\",\n                \"hover:bg-background hover:text-foreground\",\n                \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1\",\n                \"[-webkit-tap-highlight-color:transparent] active:bg-background/80\"\n              )}\n              href={source.url}\n              onClick={(e) => e.stopPropagation()}\n              rel=\"noopener noreferrer\"\n              target=\"_blank\"\n            >\n              <ExternalLink aria-hidden=\"true\" className=\"size-3.5\" />\n            </a>\n          </div>\n        </div>\n        {source.snippet && (\n          <CollapsibleContent className=\"overflow-hidden transition-all motion-safe:duration-200 motion-reduce:transition-none\">\n            <SourceSnippet snippet={source.snippet} />\n          </CollapsibleContent>\n        )}\n      </article>\n    </Collapsible>\n  );\n}\n\ninterface CitationItemHeaderProps {\n  citation: Citation;\n  isOpen: boolean;\n  onToggle: () => void;\n}\n\nfunction CitationItemHeader({\n  citation,\n  isOpen,\n  onToggle,\n}: CitationItemHeaderProps) {\n  const hasMultipleSources = citation.sources.length > 1;\n  const sourceCount = citation.sources.length;\n\n  if (hasMultipleSources) {\n    return (\n      <CollapsibleTrigger\n        aria-controls={`citation-content-${citation.id}`}\n        aria-expanded={isOpen}\n        className={cn(\n          \"flex min-h-[44px] items-center justify-between rounded-lg bg-card px-2 text-left transition-colors\",\n          \"hover:bg-accent/50\",\n          \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n          \"touch-manipulation [-webkit-tap-highlight-color:transparent] active:bg-accent/70 sm:min-h-[24px]\"\n        )}\n        type=\"button\"\n      >\n        <div className=\"flex items-center gap-3\">\n          <CitationNumber\n            aria-label={`Citation ${citation.number}`}\n            asButton={false}\n            number={citation.number}\n          />\n          <div className=\"flex flex-col gap-0.5\">\n            <span className=\"font-semibold text-sm tabular-nums\">\n              {sourceCount} {sourceCount === 1 ? \"source\" : \"sources\"}\n            </span>\n            {citation.text && (\n              <span className=\"text-muted-foreground text-xs leading-tight\">\n                {citation.text}\n              </span>\n            )}\n          </div>\n        </div>\n        <ChevronDown\n          aria-hidden=\"true\"\n          className={cn(\n            \"size-4 shrink-0 text-muted-foreground transition-transform motion-safe:duration-200\",\n            isOpen && \"rotate-180\"\n          )}\n        />\n      </CollapsibleTrigger>\n    );\n  }\n\n  return (\n    <div\n      aria-label={`Citation ${citation.number}`}\n      className=\"flex items-center gap-3 rounded-lg bg-card px-2\"\n      role=\"group\"\n    >\n      <CitationNumber\n        aria-label={`Citation ${citation.number}`}\n        asButton={false}\n        number={citation.number}\n      />\n      <div className=\"flex flex-col gap-0.5\">\n        <span className=\"font-semibold text-sm\">Source</span>\n        {citation.text && (\n          <span className=\"text-muted-foreground text-xs leading-tight\">\n            {citation.text}\n          </span>\n        )}\n      </div>\n    </div>\n  );\n}\n\ninterface CitationItemProps {\n  citation: Citation;\n  onSourceClick?: (source: CitationSource) => void;\n  defaultExpanded?: boolean;\n}\n\nfunction CitationItem({\n  citation,\n  onSourceClick,\n  defaultExpanded = false,\n}: CitationItemProps) {\n  const [isOpen, setIsOpen] = useState(defaultExpanded);\n  const hasMultipleSources = citation.sources.length > 1;\n\n  const handleToggle = useCallback(() => {\n    setIsOpen((prev) => !prev);\n  }, []);\n\n  if (citation.sources.length === 0) {\n    return null;\n  }\n\n  return (\n    <Collapsible onOpenChange={setIsOpen} open={isOpen}>\n      <div className=\"flex flex-col gap-3\">\n        <CitationItemHeader\n          citation={citation}\n          isOpen={isOpen}\n          onToggle={handleToggle}\n        />\n\n        {hasMultipleSources ? (\n          <CollapsibleContent\n            className=\"overflow-hidden transition-all motion-safe:duration-200 motion-reduce:transition-none\"\n            id={`citation-content-${citation.id}`}\n          >\n            <ul\n              aria-label={`Sources for citation ${citation.number}`}\n              className=\"ml-6 flex flex-col gap-2 border-l pl-6\"\n              role=\"list\"\n            >\n              {citation.sources.map((source) => (\n                <li key={source.id} role=\"listitem\">\n                  <SourcePreview\n                    citationNumber={citation.number}\n                    onSourceClick={onSourceClick}\n                    source={source}\n                  />\n                </li>\n              ))}\n            </ul>\n          </CollapsibleContent>\n        ) : (\n          <div className=\"ml-6 border-l pl-6\">\n            <SourcePreview\n              citationNumber={citation.number}\n              onSourceClick={onSourceClick}\n              source={citation.sources[0]}\n            />\n          </div>\n        )}\n      </div>\n    </Collapsible>\n  );\n}\n\ninterface CitationsHeaderProps {\n  count: number;\n}\n\nfunction CitationsHeader({ count }: CitationsHeaderProps) {\n  return (\n    <header className=\"flex items-center gap-2\">\n      <div\n        aria-hidden=\"true\"\n        className=\"flex size-6 items-center justify-center rounded-lg bg-primary/10\"\n      >\n        <Quote className=\"size-3.5 text-primary\" />\n      </div>\n      <h2 className=\"font-semibold text-sm\">Sources & Citations</h2>\n      <span className=\"text-muted-foreground text-xs tabular-nums\">\n        ({count})\n      </span>\n    </header>\n  );\n}\n\ninterface CitationsListProps {\n  citations: Citation[];\n  onSourceClick?: (source: CitationSource) => void;\n  defaultExpanded?: boolean;\n}\n\nfunction CitationsList({\n  citations,\n  onSourceClick,\n  defaultExpanded = false,\n}: CitationsListProps) {\n  const validCitations = useMemo(\n    () => citations.filter((c) => c.sources.length > 0),\n    [citations]\n  );\n\n  if (validCitations.length === 0) {\n    return null;\n  }\n\n  return (\n    <ul aria-label=\"Citations list\" className=\"flex flex-col gap-3\" role=\"list\">\n      {validCitations.map((citation) => (\n        <li key={citation.id} role=\"listitem\">\n          <CitationItem\n            citation={citation}\n            defaultExpanded={defaultExpanded}\n            onSourceClick={onSourceClick}\n          />\n        </li>\n      ))}\n    </ul>\n  );\n}\n\ninterface CitationsEmptyStateProps {\n  className?: string;\n}\n\nfunction CitationsEmptyState({ className }: CitationsEmptyStateProps) {\n  return (\n    <Empty className={className}>\n      <EmptyHeader>\n        <EmptyMedia variant=\"icon\">\n          <Quote aria-hidden=\"true\" className=\"size-6\" />\n        </EmptyMedia>\n        <EmptyTitle>No citations available</EmptyTitle>\n        <EmptyDescription>\n          Citations will appear here when sources are referenced in the\n          conversation.\n        </EmptyDescription>\n      </EmptyHeader>\n    </Empty>\n  );\n}\n\nexport default function AICitations({\n  citations,\n  className,\n  defaultExpanded = false,\n  onSourceClick,\n}: AICitationsProps) {\n  const validCitations = useMemo(\n    () => citations?.filter((c) => c.sources && c.sources.length > 0) || [],\n    [citations]\n  );\n\n  const handleSourceClick = useCallback(\n    (source: CitationSource) => {\n      onSourceClick?.(source);\n    },\n    [onSourceClick]\n  );\n\n  if (!citations || validCitations.length === 0) {\n    return <CitationsEmptyState className={className} />;\n  }\n\n  return (\n    <section\n      aria-label=\"Sources and citations\"\n      className={cn(\"flex w-full flex-col gap-3\", className)}\n    >\n      <CitationsHeader count={validCitations.length} />\n      <CitationsList\n        citations={validCitations}\n        defaultExpanded={defaultExpanded}\n        onSourceClick={handleSourceClick}\n      />\n    </section>\n  );\n}\n\ninterface InlineCitationProps {\n  number: number;\n  onClick?: () => void;\n}\n\nfunction InlineCitation({ number, onClick }: InlineCitationProps) {\n  return (\n    <CitationNumber\n      aria-label={`View citation ${number}`}\n      className=\"relative -top-0.5 mx-0.5\"\n      number={number}\n      onClick={onClick}\n    />\n  );\n}\n\nexport { InlineCitation, CitationNumber };\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "block",
    "ai"
  ],
  "type": "registry:ui"
}