{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"citations","type":"registry:component","title":"Citations","description":"Inline citation markers paired with a collapsible, progressively rendered reference collection for grounded agent responses.","author":"Saurabh <saurabh10102@gmail.com>","dependencies":["clsx","lucide-react","motion","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/agents/citations.tsx","type":"registry:component","target":"@components/agents/citations.tsx","content":"\"use client\";\n// beui.dev/components/agents/citations\n\nimport { BookOpenText, ChevronDown, ExternalLink, Globe2 } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport {\n  type ReactNode,\n  useCallback,\n  useId,\n  useState,\n} from \"react\";\nimport { AgentDisclosure } from \"@/components/agents/agent-disclosure\";\nimport { EASE_OUT, SPRING_LAYOUT, SPRING_SWAP } from \"@/lib/ease\";\nimport { getFaviconUrl } from \"@/lib/favicon\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface CitationItem {\n  id: string;\n  title: ReactNode;\n  domain?: ReactNode;\n  url?: string;\n}\n\nexport interface CitationsProps {\n  citations: CitationItem[];\n  title?: ReactNode;\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  idPrefix?: string;\n  className?: string;\n}\n\nexport interface CitationProps {\n  citationId: string;\n  index: number;\n  /** Must match the related Citations idPrefix. */\n  idPrefix: string;\n  className?: string;\n}\n\nexport interface CitationListProps {\n  citations: CitationItem[];\n  idPrefix?: string;\n  className?: string;\n}\n\nexport interface CitationStackProps {\n  citations: CitationItem[];\n  limit?: number;\n  className?: string;\n}\n\nfunction citationTargetId(prefix: string, citationId: string) {\n  return `${prefix}-${citationId.replace(/[^a-zA-Z0-9_-]/g, \"-\")}`;\n}\n\nexport function Citation({\n  citationId,\n  index,\n  idPrefix,\n  className,\n}: CitationProps) {\n  return (\n    <a\n      href={`#${citationTargetId(idPrefix, citationId)}`}\n      aria-label={`View citation ${index}`}\n      className={cn(\n        \"mx-0.5 inline-flex min-w-4 -translate-y-0.5 items-center justify-center rounded-md bg-muted/60 px-1 py-0.5 text-[10px] font-semibold leading-none text-muted-foreground no-underline outline-none transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring\",\n        className,\n      )}\n    >\n      {index}\n    </a>\n  );\n}\n\nexport function CitationFavicon({\n  url,\n  className,\n}: {\n  url?: string;\n  className?: string;\n}) {\n  const favicon = url ? getFaviconUrl(url) : null;\n  const [failedUrl, setFailedUrl] = useState<string | null>(null);\n\n  return (\n    <span\n      aria-hidden=\"true\"\n      className={cn(\n        \"grid size-5 shrink-0 place-items-center text-muted-foreground\",\n        className,\n      )}\n    >\n      {favicon && failedUrl !== favicon ? (\n        // biome-ignore lint/performance/noImgElement: Dynamic cross-site favicons keep this framework-agnostic registry component portable.\n        <img\n          src={favicon}\n          alt=\"\"\n          width={16}\n          height={16}\n          referrerPolicy=\"no-referrer\"\n          onError={() => setFailedUrl(favicon)}\n          className=\"size-4 rounded-sm object-contain\"\n        />\n      ) : (\n        <Globe2 className=\"size-3.5\" />\n      )}\n    </span>\n  );\n}\n\nexport function CitationStack({\n  citations,\n  limit = 3,\n  className,\n}: CitationStackProps) {\n  return (\n    <span\n      aria-hidden=\"true\"\n      className={cn(\"flex -space-x-1.5\", className)}\n    >\n      {citations.slice(0, limit).map((citation) => (\n        <CitationFavicon\n          key={citation.id}\n          url={citation.url}\n          className=\"size-6 rounded-full bg-background ring-2 ring-background\"\n        />\n      ))}\n    </span>\n  );\n}\n\nfunction CitationRow({\n  citation,\n  index,\n  idPrefix,\n}: {\n  citation: CitationItem;\n  index: number;\n  idPrefix: string;\n}) {\n  const content = (\n    <>\n      <CitationFavicon url={citation.url} />\n      <span className=\"flex min-w-0 flex-1 flex-wrap items-baseline gap-x-2 gap-y-0.5\">\n        <span className=\"truncate text-sm font-medium text-foreground/80 transition-colors group-hover/citation:text-foreground\">\n          {citation.title}\n        </span>\n        {citation.domain ? (\n          <span className=\"min-w-0 truncate text-xs text-muted-foreground/60\">\n            {citation.domain}\n          </span>\n        ) : null}\n      </span>\n      <span className=\"flex shrink-0 items-center gap-1.5\">\n        <span className=\"grid size-5 place-items-center rounded-md bg-foreground/[0.05] text-[10px] font-semibold tabular-nums text-muted-foreground\">\n          {index}\n        </span>\n        {citation.url ? (\n          <ExternalLink className=\"size-3.5 text-muted-foreground/40 transition-colors group-hover/citation:text-muted-foreground\" />\n        ) : null}\n      </span>\n    </>\n  );\n  const className =\n    \"group/citation flex items-center gap-2 rounded-md px-1.5 py-1 outline-none focus-visible:ring-2 focus-visible:ring-ring\";\n  const id = citationTargetId(idPrefix, citation.id);\n\n  return citation.url ? (\n    <a\n      id={id}\n      href={citation.url}\n      target=\"_blank\"\n      rel=\"noreferrer noopener\"\n      className={className}\n    >\n      {content}\n    </a>\n  ) : (\n    <div id={id} className={className}>\n      {content}\n    </div>\n  );\n}\n\nexport function CitationList({\n  citations,\n  idPrefix,\n  className,\n}: CitationListProps) {\n  const reduce = useReducedMotion() ?? false;\n  const baseId = useId();\n  const resolvedPrefix =\n    idPrefix ?? `citation-list-${baseId.replace(/:/g, \"\")}`;\n\n  return (\n    <div className={cn(\"grid gap-0.5\", className)}>\n      <AnimatePresence mode=\"popLayout\">\n        {citations.map((citation, index) => (\n          <motion.div\n            layout=\"position\"\n            key={citation.id}\n            initial={reduce ? { opacity: 1 } : { opacity: 0, y: 6 }}\n            animate={{ opacity: 1, y: 0 }}\n            exit={reduce ? { opacity: 0 } : { opacity: 0, y: -3 }}\n            transition={\n              reduce\n                ? { duration: 0 }\n                : {\n                    opacity: { duration: 0.18, ease: EASE_OUT },\n                    y: SPRING_LAYOUT,\n                    layout: SPRING_LAYOUT,\n                  }\n            }\n          >\n            <CitationRow\n              citation={citation}\n              index={index + 1}\n              idPrefix={resolvedPrefix}\n            />\n          </motion.div>\n        ))}\n      </AnimatePresence>\n    </div>\n  );\n}\n\nexport function Citations({\n  citations,\n  title = \"Sources\",\n  open,\n  defaultOpen = false,\n  onOpenChange,\n  idPrefix,\n  className,\n}: CitationsProps) {\n  const reduce = useReducedMotion() ?? false;\n  const baseId = useId();\n  const contentId = `${baseId}-content`;\n  const resolvedPrefix =\n    idPrefix ?? `citation-${baseId.replace(/:/g, \"\")}`;\n  const [internalOpen, setInternalOpen] = useState(defaultOpen);\n  const currentOpen = open ?? internalOpen;\n  const setOpen = useCallback(\n    (next: boolean) => {\n      if (open === undefined) setInternalOpen(next);\n      onOpenChange?.(next);\n    },\n    [onOpenChange, open],\n  );\n\n  return (\n    <div className={cn(\"w-full text-sm\", className)}>\n      <button\n        type=\"button\"\n        aria-expanded={currentOpen}\n        aria-controls={contentId}\n        onClick={() => setOpen(!currentOpen)}\n        className=\"group -ml-1 flex min-h-8 items-center gap-2 rounded-lg px-1 text-left text-muted-foreground outline-none transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring\"\n      >\n        <BookOpenText className=\"size-4\" />\n        <span className=\"font-medium\">{title}</span>\n        <span className=\"rounded-full bg-muted px-1.5 py-0.5 text-[10px] font-semibold tabular-nums\">\n          {citations.length}\n        </span>\n        <motion.span\n          aria-hidden=\"true\"\n          animate={{ rotate: currentOpen ? 180 : 0 }}\n          transition={reduce ? { duration: 0 } : SPRING_SWAP}\n          className=\"text-muted-foreground/60\"\n        >\n          <ChevronDown className=\"size-3.5\" />\n        </motion.span>\n      </button>\n\n      <AgentDisclosure\n        id={contentId}\n        open={currentOpen}\n      >\n        <CitationList\n          citations={citations}\n          idPrefix={resolvedPrefix}\n          className=\"mt-1\"\n        />\n      </AgentDisclosure>\n    </div>\n  );\n}\n"},{"path":"components/agents/agent-disclosure.tsx","type":"registry:component","target":"@components/agents/agent-disclosure.tsx","content":"\"use client\";\n\nimport { motion, type HTMLMotionProps, useReducedMotion } from \"motion/react\";\nimport type { CSSProperties } from \"react\";\nimport { EASE_OUT } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface AgentDisclosureProps\n  extends Omit<HTMLMotionProps<\"div\">, \"animate\" | \"initial\"> {\n  open: boolean;\n  openHeight?: CSSProperties[\"height\"];\n}\n\n/** Shared transform-only reveal for collapsible agent content. */\nexport function AgentDisclosure({\n  open,\n  openHeight = \"auto\",\n  className,\n  style,\n  transition,\n  ...props\n}: AgentDisclosureProps) {\n  const reduce = useReducedMotion() ?? false;\n\n  return (\n    <motion.div\n      {...props}\n      aria-hidden={!open}\n      inert={!open}\n      initial={false}\n      animate={\n        reduce\n          ? { opacity: open ? 1 : 0 }\n          : {\n              opacity: open ? 1 : 0,\n              clipPath: open ? \"inset(0 0 0% 0)\" : \"inset(0 0 100% 0)\",\n              y: open ? 0 : -4,\n            }\n      }\n      transition={\n        transition ?? {\n          duration: reduce ? 0 : open ? 0.22 : 0.14,\n          ease: EASE_OUT,\n        }\n      }\n      className={cn(\"overflow-hidden\", className)}\n      style={{\n        ...style,\n        height: open ? openHeight : 0,\n        pointerEvents: open ? undefined : \"none\",\n        transformOrigin: \"top\",\n      }}\n    />\n  );\n}\n"},{"path":"lib/ease.ts","type":"registry:lib","target":"@lib/ease.ts","content":"// Shared motion tokens. Easing curves mirror the CSS custom properties in\n// globals.css; springs are the canonical physics used across components.\n// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.\n\nexport const EASE_OUT = [0.16, 1, 0.3, 1] as const;\nexport const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;\nexport const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;\n\n/** CSS string form of EASE_OUT for inline style transitions. */\nexport const EASE_OUT_CSS = \"cubic-bezier(0.16, 1, 0.3, 1)\";\n\n/** Press feedback on buttons and other tappable surfaces. */\nexport const SPRING_PRESS = {\n  type: \"spring\",\n  stiffness: 500,\n  damping: 30,\n  mass: 0.6,\n} as const;\n\n/** Content swaps — label/icon slots trading places inside a control. */\nexport const SPRING_SWAP = {\n  type: \"spring\",\n  stiffness: 460,\n  damping: 30,\n  mass: 0.55,\n} as const;\n\n/** Overlay panel entrances — modals and sheets summoned by pointer. */\nexport const SPRING_PANEL = {\n  type: \"spring\",\n  stiffness: 420,\n  damping: 40,\n  mass: 0.5,\n} as const;\n\n/** Shared-layout glides — pills, indicators and panels morphing between positions. */\nexport const SPRING_LAYOUT = {\n  type: \"spring\",\n  stiffness: 360,\n  damping: 32,\n  mass: 0.6,\n} as const;\n\n/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */\nexport const SPRING_MOUSE = {\n  stiffness: 200,\n  damping: 15,\n  mass: 0.3,\n} as const;\n\n/** Dragged handles and fills (sliders) — critically damped `useSpring` config,\n * so the value follows the pointer butterily and never rebounds off an end. */\nexport const SPRING_GLIDE = {\n  stiffness: 700,\n  damping: 50,\n  mass: 0.5,\n} as const;\n"},{"path":"lib/favicon.ts","type":"registry:lib","target":"@lib/favicon.ts","content":"/** Resolve a website URL to its conventional root favicon location. */\nexport function getFaviconUrl(value: string) {\n  try {\n    return new URL(\"/favicon.ico\", value).toString();\n  } catch {\n    return null;\n  }\n}\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"}]}