{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "citation",
  "title": "Citation",
  "description": "An inline claim marker with a source preview card and destination link.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "https://uaiblocks.dev/r/uai-theme.json",
    "https://uaiblocks.dev/r/uai-utils.json"
  ],
  "files": [
    {
      "path": "src/registry/uai/components/citation.tsx",
      "content": "\"use client\";\n\nimport { ArrowUpRight } from \"lucide-react\";\nimport {\n  type ComponentProps,\n  createContext,\n  useContext,\n  useEffect,\n  useId,\n  useRef,\n  useState,\n} from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/uai-utils\";\n\nexport const CITATION_VARIANTS = [\"number\", \"chip\", \"underline\"] as const;\nexport type CitationVariant = (typeof CITATION_VARIANTS)[number];\nexport type CitationProps = ComponentProps<\"span\"> & {\n  variant?: CitationVariant;\n  /** Source number shown by the default marker. */\n  index?: number;\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n};\n\ntype CitationContextValue = {\n  id: string;\n  index?: number;\n  variant: CitationVariant;\n  open: boolean;\n  setOpen: (open: boolean) => void;\n  triggerRef: React.RefObject<HTMLButtonElement | null>;\n};\n\nconst CitationContext = createContext<CitationContextValue | null>(null);\n\nfunction useCitation(part: string) {\n  const context = useContext(CitationContext);\n  if (!context) throw new Error(`${part} must be used within Citation`);\n  return context;\n}\n\nconst CLOSE_DELAY = 120;\n\nexport function Citation({\n  variant = \"number\",\n  index,\n  open,\n  defaultOpen = false,\n  onOpenChange,\n  children,\n  className,\n  onPointerEnter,\n  onPointerLeave,\n  onBlur,\n  onKeyDown,\n  ...props\n}: CitationProps) {\n  const id = useId();\n  const triggerRef = useRef<HTMLButtonElement>(null);\n  const closeTimer = useRef<number | undefined>(undefined);\n  const [internal, setInternal] = useState(defaultOpen);\n  const visible = open ?? internal;\n  const setOpen = (next: boolean) => {\n    window.clearTimeout(closeTimer.current);\n    if (next === visible) return;\n    if (open === undefined) setInternal(next);\n    onOpenChange?.(next);\n  };\n  useEffect(() => () => window.clearTimeout(closeTimer.current), []);\n  return (\n    <CitationContext.Provider value={{ id, index, variant, open: visible, setOpen, triggerRef }}>\n      {/* biome-ignore lint/a11y/noStaticElementInteractions: hover and focus-out handlers coordinate the preview; the trigger is the interactive element. */}\n      <span\n        data-slot=\"citation\"\n        className={cn(\"relative inline\", className)}\n        {...props}\n        data-variant={variant}\n        data-state={visible ? \"open\" : \"closed\"}\n        onPointerEnter={(event) => {\n          onPointerEnter?.(event);\n          if (event.pointerType !== \"touch\") setOpen(true);\n        }}\n        onPointerLeave={(event) => {\n          onPointerLeave?.(event);\n          if (event.pointerType === \"touch\") return;\n          window.clearTimeout(closeTimer.current);\n          closeTimer.current = window.setTimeout(() => setOpen(false), CLOSE_DELAY);\n        }}\n        onBlur={(event) => {\n          onBlur?.(event);\n          const next = event.relatedTarget as Node | null;\n          if (!next || !event.currentTarget.contains(next)) setOpen(false);\n        }}\n        onKeyDown={(event) => {\n          onKeyDown?.(event);\n          if (event.defaultPrevented || event.key !== \"Escape\" || !visible) return;\n          event.preventDefault();\n          setOpen(false);\n          triggerRef.current?.focus();\n        }}\n      >\n        {children}\n      </span>\n    </CitationContext.Provider>\n  );\n}\n\nexport function CitationClaim({ className, ...props }: ComponentProps<\"span\">) {\n  const context = useCitation(\"CitationClaim\");\n  const underline = context.variant === \"underline\";\n  return (\n    <span\n      data-slot=\"citation-claim\"\n      className={cn(\n        \"rounded-[4px] decoration-dotted decoration-[1.5px] underline-offset-4\",\n        \"[transition:background-color_120ms_ease-out,box-shadow_120ms_ease-out,text-decoration-color_120ms_ease-out]\",\n        underline && \"underline\",\n        context.open ? \"decoration-muted-foreground\" : \"decoration-border-strong\",\n        underline && context.open && \"bg-foreground/7 ring-2 ring-foreground/7\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function CitationTrigger({\n  children,\n  onClick,\n  onFocus,\n  onPointerDown,\n  className,\n  ...props\n}: ComponentProps<\"button\">) {\n  const context = useCitation(\"CitationTrigger\");\n  const press = useRef<{ pointerType: string; wasOpen: boolean } | null>(null);\n  const chip = context.variant === \"chip\";\n  return (\n    <Button\n      type=\"button\"\n      variant=\"ghost\"\n      data-slot=\"citation-trigger\"\n      aria-label={children === undefined ? `Source ${context.index ?? \"\"}`.trim() : undefined}\n      className={cn(\n        \"relative mx-0.5 inline-flex shrink cursor-pointer items-center justify-center gap-1 border-0 bg-muted py-0 font-medium text-muted-foreground tabular-nums after:absolute after:-inset-1 after:content-['']\",\n        \"transition-[background-color,color,scale] duration-[120ms,120ms,140ms] ease-[ease-out,ease-out,cubic-bezier(0.23,1,0.32,1)]\",\n        \"hover:bg-[color-mix(in_oklab,var(--muted)_80%,var(--foreground))] hover:text-foreground aria-expanded:bg-[color-mix(in_oklab,var(--muted)_80%,var(--foreground))] aria-expanded:text-foreground dark:hover:bg-[color-mix(in_oklab,var(--muted)_80%,var(--foreground))]\",\n        \"focus-visible:ring-0 focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-offset-1 focus-visible:outline-ring active:scale-94\",\n        \"motion-reduce:transition-none motion-reduce:active:scale-100\",\n        chip\n          ? \"h-5 rounded-md px-[7px] align-[1px] font-mono text-[11px]/none has-[>svg]:px-[7px]\"\n          : \"h-[17px] min-w-[17px] rounded-full px-[5px] align-[2px] text-[10.5px]/none has-[>svg]:px-[5px]\",\n        className,\n      )}\n      {...props}\n      ref={context.triggerRef}\n      aria-expanded={context.open}\n      aria-controls={`${context.id}-source`}\n      onFocus={(event) => {\n        onFocus?.(event);\n        if (!event.defaultPrevented) context.setOpen(true);\n      }}\n      onPointerDown={(event) => {\n        onPointerDown?.(event);\n        press.current = { pointerType: event.pointerType, wasOpen: context.open };\n      }}\n      onClick={(event) => {\n        onClick?.(event);\n        const pointer = press.current;\n        press.current = null;\n        if (event.defaultPrevented) return;\n        // Mouse hover already previews the source, so a click keeps it open.\n        if (pointer?.pointerType === \"mouse\") context.setOpen(true);\n        else if (pointer) context.setOpen(!pointer.wasOpen);\n        else context.setOpen(!context.open);\n      }}\n    >\n      {children ?? context.index}\n    </Button>\n  );\n}\n\nexport function CitationPopover({ children, className, ...props }: ComponentProps<\"span\">) {\n  const context = useCitation(\"CitationPopover\");\n  return (\n    // biome-ignore lint/a11y/useSemanticElements: the preview sits inside phrasing content, where a fieldset is invalid.\n    <span\n      role=\"group\"\n      aria-labelledby={`${context.id}-title`}\n      data-slot=\"citation-popover\"\n      className={cn(\n        \"absolute top-[calc(100%+8px)] left-0 z-20 w-[300px] max-w-[calc(100vw-32px)] origin-top-left gap-1 rounded-[14px] bg-popover p-1 text-left text-[13px]/[18px] font-normal whitespace-normal text-popover-foreground\",\n        \"shadow-[0_0_0_1px_var(--border-strong),0_16px_32px_-12px_rgb(0_0_0/0.28),0_4px_8px_-4px_rgb(0_0_0/0.12)]\",\n        context.open\n          ? \"grid animate-in fade-in-0 zoom-in-96 duration-180 ease-out-quint motion-reduce:animate-none\"\n          : \"hidden\",\n        className,\n      )}\n      {...props}\n      id={`${context.id}-source`}\n      hidden={!context.open}\n    >\n      {children}\n    </span>\n  );\n}\n\nexport function CitationSource({ className, ...props }: ComponentProps<\"span\">) {\n  useCitation(\"CitationSource\");\n  return (\n    <span\n      data-slot=\"citation-source\"\n      className={cn(\n        \"flex items-center gap-1.5 px-2 pt-2 text-[11.5px]/4 text-subtle-foreground tabular-nums\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function CitationTitle({ className, ...props }: ComponentProps<\"span\">) {\n  const context = useCitation(\"CitationTitle\");\n  return (\n    <span\n      data-slot=\"citation-title\"\n      className={cn(\"block px-2 text-[13px]/[18px] font-medium text-balance\", className)}\n      {...props}\n      id={`${context.id}-title`}\n    />\n  );\n}\n\nexport function CitationExcerpt({ className, ...props }: ComponentProps<\"span\">) {\n  useCitation(\"CitationExcerpt\");\n  return (\n    <span\n      data-slot=\"citation-excerpt\"\n      className={cn(\n        \"mx-2 mb-1.5 line-clamp-3 max-h-[54px] text-[12.5px]/[18px] text-muted-foreground\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function CitationLink({ children, className, ...props }: ComponentProps<\"a\">) {\n  useCitation(\"CitationLink\");\n  return (\n    <a\n      data-slot=\"citation-link\"\n      className={cn(\n        \"flex min-h-8 items-center justify-between gap-2 rounded-[10px] bg-secondary pr-2 pl-2.5 text-[12.5px] font-medium text-secondary-foreground no-underline\",\n        \"transition-[background-color] duration-120 ease-[ease-out] hover:bg-[color-mix(in_oklab,var(--secondary)_85%,var(--foreground))]\",\n        \"focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ring motion-reduce:transition-none\",\n        className,\n      )}\n      {...props}\n    >\n      <span className=\"min-w-0 overflow-hidden text-ellipsis\">{children ?? \"Open source\"}</span>\n      <ArrowUpRight\n        size={14}\n        strokeWidth={1.75}\n        aria-hidden=\"true\"\n        className=\"flex-none text-muted-foreground size-3.5\"\n      />\n    </a>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/uai/citation.tsx"
    }
  ],
  "categories": [
    "ai",
    "sources"
  ],
  "type": "registry:ui"
}