{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "inline-citation",
  "type": "registry:component",
  "title": "InlineCitation",
  "description": "Inline multi-source citation with a keyboard-accessible preview and source navigation.",
  "dependencies": [
    "lucide-react@^1.27.0"
  ],
  "registryDependencies": [
    "http://127.0.0.1:3000/r/button.json",
    "http://127.0.0.1:3000/r/core.json",
    "http://127.0.0.1:3000/r/popover.json",
    "http://127.0.0.1:3000/r/source-badge.json"
  ],
  "files": [
    {
      "path": "src/registry/sources/control-ui/inline-citation.tsx",
      "target": "@components/control-ui/inline-citation.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { ArrowLeft, ArrowRight, ExternalLink } from \"lucide-react\";\nimport type { ComponentProps, CSSProperties, ReactNode, RefObject } from \"react\";\nimport { createContext, useContext, useRef, useState } from \"react\";\n\nimport type { SourceReference } from \"@/components/control-ui/contracts\";\nimport { cn } from \"@/components/control-ui/lib/cn\";\nimport { skinSlot } from \"@/components/control-ui/skin\";\nimport { SourceFavicon, sourceHostname } from \"@/components/control-ui/source-badge\";\nimport { Button } from \"@/components/control-ui/ui/button\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/control-ui/ui/popover\";\n\ntype SlideDirection = \"left\" | \"right\";\n\ntype EnteringSlide = { direction: SlideDirection; previousHeight?: number };\n\ntype ExitingSlide = { id: number; direction: SlideDirection; source: SourceReference };\n\ntype InlineCitationContextValue = {\n  currentIndex: number;\n  currentSource?: SourceReference;\n  entering: EnteringSlide | null;\n  exiting: ExitingSlide | null;\n  endExit: (id: number) => void;\n  hasNext: boolean;\n  hasPrevious: boolean;\n  next: () => void;\n  previous: () => void;\n  sourceRef: RefObject<HTMLElement | null>;\n  sources: readonly SourceReference[];\n};\n\nconst InlineCitationContext = createContext<InlineCitationContextValue | null>(null);\n\nfunction useInlineCitation() {\n  const context = useContext(InlineCitationContext);\n  if (!context) throw new Error(\"InlineCitation parts must be rendered inside <InlineCitation>.\");\n  return context;\n}\n\ntype PopoverRootProps = ComponentProps<typeof Popover>;\n\nexport type InlineCitationProps = ComponentProps<\"span\"> & {\n  sources: readonly SourceReference[];\n  open?: PopoverRootProps[\"open\"];\n  defaultOpen?: PopoverRootProps[\"defaultOpen\"];\n  onOpenChange?: PopoverRootProps[\"onOpenChange\"];\n};\n\n// No anchor positioning = no hold animation = nothing ever unmounts clone.\nfunction supportsCrossSlide() {\n  return typeof CSS !== \"undefined\" && CSS.supports(\"(anchor-name: --aui-slide-panel) and (anchor-scope: --aui-slide-panel)\");\n}\n\nexport function InlineCitation({ sources, open, defaultOpen, onOpenChange, className, children, ...props }: InlineCitationProps) {\n  const [selectedIndex, setSelectedIndex] = useState(0);\n  const [entering, setEntering] = useState<EnteringSlide | null>(null);\n  const [exiting, setExiting] = useState<ExitingSlide | null>(null);\n  const sourceRef = useRef<HTMLElement | null>(null);\n  const exitCount = useRef(0);\n  const clearEntering = useRef(0);\n  const lastIndex = Math.max(0, sources.length - 1);\n  const currentIndex = Math.min(selectedIndex, lastIndex);\n  const currentSource = sources[currentIndex];\n  const hasPrevious = currentIndex > 0;\n  const hasNext = currentIndex < lastIndex;\n\n  // no primitive owns this swap, so lifecycle attributes theme.css keys on are stamped by hand\n  // height is measured pre-commit — last frame on which old box still exists\n  const move = (step: 1 | -1) => {\n    const nextIndex = Math.min(Math.max(currentIndex + step, 0), lastIndex);\n    if (nextIndex === currentIndex) return;\n    const direction: SlideDirection = step > 0 ? \"right\" : \"left\";\n    const leaving = sources[currentIndex];\n\n    setSelectedIndex(nextIndex);\n    setEntering({ direction, previousHeight: sourceRef.current?.getBoundingClientRect().height });\n    if (leaving && supportsCrossSlide()) {\n      exitCount.current += 1;\n      setExiting({ id: exitCount.current, direction, source: leaving });\n    }\n\n    cancelAnimationFrame(clearEntering.current);\n    clearEntering.current = requestAnimationFrame(() => {\n      clearEntering.current = requestAnimationFrame(() => setEntering(null));\n    });\n  };\n\n  const handleOpenChange: NonNullable<PopoverRootProps[\"onOpenChange\"]> = (...args) => {\n    const [nextOpen] = args;\n    // content unmounts on close, so slide caught mid-flight would resume on next open\n    if (!nextOpen) {\n      setEntering(null);\n      setExiting(null);\n    }\n    onOpenChange?.(...args);\n  };\n\n  const context = {\n    currentIndex,\n    currentSource,\n    entering,\n    exiting,\n    endExit: (id: number) => setExiting((current) => (current?.id === id ? null : current)),\n    hasNext,\n    hasPrevious,\n    next: () => move(1),\n    previous: () => move(-1),\n    sourceRef,\n    sources,\n  } satisfies InlineCitationContextValue;\n\n  return (\n    <InlineCitationContext.Provider value={context}>\n      <span\n        data-control-ui=\"inline-citation\"\n        data-slot=\"root\"\n        {...props}\n        className={cn(\"not-prose inline-flex align-baseline\", skinSlot(\"inline-citation\", \"root\", {}), className)}\n      >\n        <Popover open={open} defaultOpen={defaultOpen} onOpenChange={handleOpenChange}>\n          {children ?? (\n            <>\n              <InlineCitationTrigger />\n              <InlineCitationContent />\n            </>\n          )}\n        </Popover>\n      </span>\n    </InlineCitationContext.Provider>\n  );\n}\n\nexport type InlineCitationTriggerProps = ComponentProps<typeof PopoverTrigger>;\n\nexport function InlineCitationTrigger({\n  \"aria-label\": ariaLabel,\n  className,\n  children,\n  disabled,\n  render,\n  ...props\n}: InlineCitationTriggerProps) {\n  const { sources } = useInlineCitation();\n  const firstSource = sources[0];\n  const sourceCount = sources.length;\n  const accessibleLabel = sourceCount === 1 ? \"View 1 source\" : `View ${sourceCount} sources`;\n\n  return (\n    <PopoverTrigger\n      render={render ?? <button type=\"button\" />}\n      aria-label={ariaLabel ?? accessibleLabel}\n      {...props}\n      disabled={disabled || !firstSource}\n      data-control-ui=\"inline-citation\"\n      data-slot=\"trigger\"\n      className={cn(\n        \"ml-1 inline-flex h-6 max-w-52 items-center gap-1.5 rounded-full border border-border bg-muted/55 px-1.5 pr-2 align-baseline text-caption font-normal text-muted-foreground outline-none transition-colors duration-[var(--duration-fast)] hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring\",\n        skinSlot(\"inline-citation\", \"trigger\", {}),\n        className,\n      )}\n    >\n      {children ?? (\n        <>\n          <InlineCitationFavicons />\n          <InlineCitationLabel />\n        </>\n      )}\n    </PopoverTrigger>\n  );\n}\n\nexport type InlineCitationFaviconsProps = ComponentProps<\"span\"> & {\n  limit?: number;\n};\n\nexport function InlineCitationFavicons({ limit = 3, className, ...props }: InlineCitationFaviconsProps) {\n  const { sources } = useInlineCitation();\n  return (\n    <span\n      aria-hidden=\"true\"\n      data-control-ui=\"inline-citation\"\n      data-slot=\"favicons\"\n      {...props}\n      className={cn(\"flex shrink-0 -space-x-1\", skinSlot(\"inline-citation\", \"favicons\", {}), className)}\n    >\n      {sources.slice(0, limit).map((source) => (\n        <SourceFavicon\n          key={source.href}\n          data-control-ui=\"inline-citation\"\n          data-slot=\"favicon\"\n          href={source.href}\n          faviconSrc={source.faviconSrc}\n          className={cn(\"size-3.5 rounded-full border border-background bg-muted\", skinSlot(\"inline-citation\", \"favicon\", {}))}\n        />\n      ))}\n    </span>\n  );\n}\n\nexport type InlineCitationLabelProps = ComponentProps<\"span\">;\n\nexport function InlineCitationLabel({ className, children, ...props }: InlineCitationLabelProps) {\n  const { sources } = useInlineCitation();\n  const firstSource = sources[0];\n  const additionalSourceCount = Math.max(0, sources.length - 1);\n  return (\n    <span\n      data-control-ui=\"inline-citation\"\n      data-slot=\"label\"\n      {...props}\n      className={cn(\"min-w-0 truncate\", skinSlot(\"inline-citation\", \"label\", {}), className)}\n    >\n      {children ??\n        (firstSource\n          ? `${sourceHostname(firstSource.href)}${additionalSourceCount > 0 ? ` +${additionalSourceCount}` : \"\"}`\n          : \"No sources\")}\n    </span>\n  );\n}\n\nexport type InlineCitationContentProps = ComponentProps<typeof PopoverContent>;\n\nexport function InlineCitationContent({ className, children, align = \"start\", padding = \"none\", ...props }: InlineCitationContentProps) {\n  return (\n    <PopoverContent\n      align={align}\n      padding={padding}\n      {...props}\n      data-control-ui=\"inline-citation\"\n      data-slot=\"content\"\n      data-slide=\"scope\"\n      className={cn(\"w-[min(24rem,calc(100vw-2rem))] overflow-hidden\", skinSlot(\"inline-citation\", \"content\", {}), className)}\n    >\n      {children ?? (\n        <>\n          <InlineCitationNavigation />\n          <InlineCitationSource />\n        </>\n      )}\n    </PopoverContent>\n  );\n}\n\nexport type InlineCitationNavigationProps = ComponentProps<\"div\">;\n\nexport function InlineCitationNavigation({ className, children, ...props }: InlineCitationNavigationProps) {\n  return (\n    <div\n      data-control-ui=\"inline-citation\"\n      data-slot=\"navigation\"\n      {...props}\n      className={cn(\n        \"flex min-h-10 items-center gap-1 border-b border-border bg-muted/45 px-2\",\n        skinSlot(\"inline-citation\", \"navigation\", {}),\n        className,\n      )}\n    >\n      {children ?? (\n        <>\n          <InlineCitationPrevious />\n          <InlineCitationNext />\n          <InlineCitationPosition />\n        </>\n      )}\n    </div>\n  );\n}\n\nexport type InlineCitationPreviousProps = Omit<ComponentProps<typeof Button>, \"children\"> & {\n  children?: ReactNode;\n};\n\nexport function InlineCitationPrevious({ className, children, disabled, onClick, ...props }: InlineCitationPreviousProps) {\n  const { hasPrevious, previous } = useInlineCitation();\n  return (\n    <Button\n      aria-label=\"Previous source\"\n      size=\"xs\"\n      variant=\"quiet\"\n      iconOnly\n      {...props}\n      disabled={disabled || !hasPrevious}\n      onClick={(event) => {\n        onClick?.(event);\n        if (!event.defaultPrevented) previous();\n      }}\n      data-control-ui=\"inline-citation\"\n      data-slot=\"previous\"\n      className={cn(skinSlot(\"inline-citation\", \"previous\", {}), className)}\n    >\n      {children ?? <ArrowLeft aria-hidden=\"true\" />}\n    </Button>\n  );\n}\n\nexport type InlineCitationNextProps = Omit<ComponentProps<typeof Button>, \"children\"> & {\n  children?: ReactNode;\n};\n\nexport function InlineCitationNext({ className, children, disabled, onClick, ...props }: InlineCitationNextProps) {\n  const { hasNext, next } = useInlineCitation();\n  return (\n    <Button\n      aria-label=\"Next source\"\n      size=\"xs\"\n      variant=\"quiet\"\n      iconOnly\n      {...props}\n      disabled={disabled || !hasNext}\n      onClick={(event) => {\n        onClick?.(event);\n        if (!event.defaultPrevented) next();\n      }}\n      data-control-ui=\"inline-citation\"\n      data-slot=\"next\"\n      className={cn(skinSlot(\"inline-citation\", \"next\", {}), className)}\n    >\n      {children ?? <ArrowRight aria-hidden=\"true\" />}\n    </Button>\n  );\n}\n\nexport type InlineCitationPositionProps = ComponentProps<\"span\">;\n\nexport function InlineCitationPosition({ className, children, ...props }: InlineCitationPositionProps) {\n  const { currentIndex, sources } = useInlineCitation();\n  return (\n    <span\n      role=\"status\"\n      aria-live=\"polite\"\n      aria-atomic=\"true\"\n      data-control-ui=\"inline-citation\"\n      data-slot=\"position\"\n      {...props}\n      className={cn(\"ml-auto px-1 text-caption tabular-nums text-muted-foreground\", skinSlot(\"inline-citation\", \"position\", {}), className)}\n    >\n      {children ?? `${sources.length === 0 ? 0 : currentIndex + 1}/${sources.length}`}\n    </span>\n  );\n}\n\nexport type InlineCitationSourceProps = ComponentProps<\"article\"> & {\n  source?: SourceReference;\n};\n\ntype SourcePanelStyle = CSSProperties & {\n  \"--aui-slide-prev-height\"?: string;\n};\n\nfunction InlineCitationSourceDetails({ source }: { source: SourceReference }) {\n  const hostname = sourceHostname(source.href);\n\n  return (\n    <>\n      <div\n        data-control-ui=\"inline-citation\"\n        data-slot=\"source-header\"\n        className={cn(\"flex items-center gap-2\", skinSlot(\"inline-citation\", \"source-header\", {}))}\n      >\n        <SourceFavicon\n          data-control-ui=\"inline-citation\"\n          data-slot=\"source-favicon\"\n          href={source.href}\n          faviconSrc={source.faviconSrc}\n          className={cn(\"size-5 rounded-full bg-muted\", skinSlot(\"inline-citation\", \"source-favicon\", {}))}\n        />\n        <span className=\"min-w-0 truncate text-caption text-muted-foreground\">{hostname}</span>\n      </div>\n      <a\n        data-control-ui=\"inline-citation\"\n        data-slot=\"source-title\"\n        href={source.href}\n        target=\"_blank\"\n        rel=\"noreferrer noopener\"\n        className={cn(\n          \"group/source-title flex min-w-0 items-start gap-2 text-label font-medium leading-5 text-foreground outline-none hover:underline focus-visible:underline\",\n          skinSlot(\"inline-citation\", \"source-title\", {}),\n        )}\n      >\n        <span className=\"min-w-0 flex-1 wrap-anywhere\">{source.title ?? hostname}</span>\n        <ExternalLink aria-hidden=\"true\" className=\"mt-0.5 size-3.5 shrink-0 text-muted-foreground\" />\n      </a>\n      {source.description ? (\n        <p\n          data-control-ui=\"inline-citation\"\n          data-slot=\"source-description\"\n          className={cn(\"line-clamp-3 text-caption leading-5 text-muted-foreground\", skinSlot(\"inline-citation\", \"source-description\", {}))}\n        >\n          {source.description}\n        </p>\n      ) : null}\n      {source.quote ? (\n        <blockquote\n          data-control-ui=\"inline-citation\"\n          data-slot=\"source-quote\"\n          className={cn(\n            \"rounded-[var(--radius-control)] bg-muted/55 px-3 py-2 text-caption leading-5 text-muted-foreground\",\n            skinSlot(\"inline-citation\", \"source-quote\", {}),\n          )}\n        >\n          “{source.quote}”\n        </blockquote>\n      ) : null}\n    </>\n  );\n}\n\nexport function InlineCitationSource({ source: sourceProp, className, children, style, ref, ...props }: InlineCitationSourceProps) {\n  const { currentIndex, currentSource, entering, exiting, endExit, sourceRef } = useInlineCitation();\n  const source = sourceProp ?? currentSource;\n  // custom children read CURRENT source, so cloning them would send incoming content out instead of outgoing one\n  const slides = !sourceProp && !children;\n  const panelClassName = cn(\"grid gap-3 p-4\", skinSlot(\"inline-citation\", \"source\", {}), className);\n  const enteringStyle: SourcePanelStyle | undefined =\n    slides && entering?.previousHeight ? { \"--aui-slide-prev-height\": `${entering.previousHeight}px`, ...style } : style;\n\n  if (!source) return null;\n\n  return (\n    <>\n      <article\n        // remounted per source: starting translate must be element's FIRST value, or transition eases into it instead of jumping there\n        key={`source-${currentIndex}`}\n        data-control-ui=\"inline-citation\"\n        data-slot=\"source\"\n        data-slide=\"panel\"\n        data-activation-direction={slides ? entering?.direction : undefined}\n        data-starting-style={slides && entering ? \"\" : undefined}\n        {...props}\n        ref={(node) => {\n          sourceRef.current = node;\n          if (typeof ref === \"function\") ref(node);\n          else if (ref) ref.current = node;\n        }}\n        style={enteringStyle}\n        className={panelClassName}\n      >\n        {children ?? <InlineCitationSourceDetails source={source} />}\n      </article>\n      {slides && exiting ? (\n        <article\n          key={`exit-${exiting.id}`}\n          aria-hidden=\"true\"\n          inert\n          data-control-ui=\"inline-citation\"\n          data-slot=\"source\"\n          data-slide=\"panel\"\n          data-ending-style=\"\"\n          data-activation-direction={exiting.direction}\n          onAnimationEnd={() => endExit(exiting.id)}\n          className={panelClassName}\n        >\n          <InlineCitationSourceDetails source={exiting.source} />\n        </article>\n      ) : null}\n    </>\n  );\n}\n"
    }
  ],
  "meta": {}
}
