{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "model-comparison",
  "type": "registry:component",
  "title": "Model Comparison",
  "description": "Side-by-side comparison of AI model responses with optional blind mode, metadata stats, and a vote bar.",
  "dependencies": [
    "@vllnt/ui@^0.2.1",
    "lucide-react"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/model-comparison/model-comparison.tsx",
      "content": "\"use client\";\n\nimport {\n  type ComponentPropsWithoutRef,\n  createContext,\n  forwardRef,\n  type ReactNode,\n  useCallback,\n  useContext,\n  useId,\n  useMemo,\n  useState,\n} from \"react\";\n\nimport { Eye, EyeOff } from \"lucide-react\";\n\nimport { cn } from \"@vllnt/ui\";\nimport { Badge } from \"@vllnt/ui\";\nimport { Button } from \"@vllnt/ui\";\n\nconst HIDDEN_LABEL_PREFIX = \"Model\";\n\n/**\n * Localizable strings for {@link ModelComparison}.\n *\n * @public\n */\nexport type ModelComparisonLabels = {\n  /** Caption for the cost stat. Defaults to `\"Cost\"`. */\n  cost?: string;\n  /** Caption for the blind-mode toggle when on. Defaults to `\"Hide models\"`. */\n  hide?: string;\n  /** Caption for the latency stat. Defaults to `\"Latency\"`. */\n  latency?: string;\n  /** Caption for the prompt heading. Defaults to `\"Prompt\"`. */\n  prompt?: string;\n  /** Caption for the blind-mode toggle when off. Defaults to `\"Reveal models\"`. */\n  reveal?: string;\n  /** Caption for the token count stat. Defaults to `\"Tokens\"`. */\n  tokens?: string;\n  /** Caption for the vote heading. Defaults to `\"Which response is better?\"`. */\n  voteHeading?: string;\n  /** Caption for the \"tie\" vote option. Defaults to `\"Tie\"`. */\n  voteTie?: string;\n};\n\nconst DEFAULT_LABELS = {\n  cost: \"Cost\",\n  hide: \"Hide models\",\n  latency: \"Latency\",\n  prompt: \"Prompt\",\n  reveal: \"Reveal models\",\n  tokens: \"Tokens\",\n  voteHeading: \"Which response is better?\",\n  voteTie: \"Tie\",\n} as const satisfies Required<ModelComparisonLabels>;\n\ntype ModelComparisonContextValue = {\n  blind: boolean;\n  labels: Required<ModelComparisonLabels>;\n};\n\nconst DEFAULT_CONTEXT: ModelComparisonContextValue = {\n  blind: false,\n  labels: DEFAULT_LABELS,\n};\n\nconst ModelComparisonContext = createContext(DEFAULT_CONTEXT);\n\n/**\n * Props for {@link ModelComparison}.\n *\n * @public\n */\nexport type ModelComparisonProps = {\n  /** When true, replaces model labels with anonymous placeholders. */\n  blindDefault?: boolean;\n  /** When true, suppresses the built-in blind-mode toggle. */\n  hideBlindToggle?: boolean;\n  /** Localizable strings. */\n  labels?: ModelComparisonLabels;\n  /** The prompt that drove all responses. Hidden when omitted. */\n  prompt?: ReactNode;\n} & ComponentPropsWithoutRef<\"section\">;\n\ntype ComparisonHeaderProps = {\n  blind: boolean;\n  hideBlindToggle: boolean;\n  labels: Required<ModelComparisonLabels>;\n  onToggleBlind: () => void;\n  prompt?: ReactNode;\n};\n\nfunction ComparisonHeader({\n  blind,\n  hideBlindToggle,\n  labels,\n  onToggleBlind,\n  prompt,\n}: ComparisonHeaderProps): ReactNode {\n  if (!prompt && hideBlindToggle) return null;\n  return (\n    <header className=\"flex items-start justify-between gap-3\">\n      {prompt ? (\n        <div className=\"flex min-w-0 flex-col gap-1\">\n          <span className=\"text-xs font-semibold uppercase tracking-wide text-muted-foreground\">\n            {labels.prompt}\n          </span>\n          <p className=\"text-sm text-foreground\">{prompt}</p>\n        </div>\n      ) : (\n        <span aria-hidden=\"true\" />\n      )}\n      {hideBlindToggle ? null : (\n        <Button\n          aria-pressed={blind}\n          onClick={onToggleBlind}\n          size=\"sm\"\n          type=\"button\"\n          variant=\"outline\"\n        >\n          {blind ? (\n            <>\n              <Eye aria-hidden=\"true\" className=\"mr-2 size-4\" />\n              {labels.reveal}\n            </>\n          ) : (\n            <>\n              <EyeOff aria-hidden=\"true\" className=\"mr-2 size-4\" />\n              {labels.hide}\n            </>\n          )}\n        </Button>\n      )}\n    </header>\n  );\n}\n\n/**\n * Side-by-side comparison of AI model responses to the same prompt.\n * Composes {@link Badge} and {@link Button}.\n *\n * @example\n * ```tsx\n * <ModelComparison prompt=\"Explain closures in JavaScript\">\n *   <ModelComparisonColumn model=\"claude-sonnet-4-6\" label=\"Sonnet\">\n *     {sonnetResponse}\n *     <ModelComparisonMeta tokens={320} latency=\"0.8s\" cost=\"$0.003\" />\n *   </ModelComparisonColumn>\n *   <ModelComparisonColumn model=\"gpt-4o\" label=\"GPT-4o\">\n *     {gptResponse}\n *     <ModelComparisonMeta tokens={410} latency=\"1.1s\" cost=\"$0.005\" />\n *   </ModelComparisonColumn>\n *   <ModelComparisonVote onVote={handleVote} />\n * </ModelComparison>\n * ```\n *\n * @public\n */\nexport const ModelComparison = forwardRef<HTMLElement, ModelComparisonProps>(\n  (props, ref) => {\n    const {\n      blindDefault = false,\n      children,\n      className,\n      hideBlindToggle = false,\n      labels,\n      prompt,\n      ...rest\n    } = props;\n    const resolvedLabels = useMemo(\n      () => ({ ...DEFAULT_LABELS, ...labels }),\n      [labels],\n    );\n    const [blind, setBlind] = useState(blindDefault);\n\n    const contextValue = useMemo<ModelComparisonContextValue>(\n      () => ({ blind, labels: resolvedLabels }),\n      [blind, resolvedLabels],\n    );\n\n    const handleToggleBlind = useCallback(() => {\n      setBlind((value) => !value);\n    }, []);\n\n    return (\n      <ModelComparisonContext.Provider value={contextValue}>\n        <section\n          className={cn(\n            \"flex flex-col gap-4 rounded-2xl border bg-background p-4\",\n            className,\n          )}\n          ref={ref}\n          {...rest}\n        >\n          <ComparisonHeader\n            blind={blind}\n            hideBlindToggle={hideBlindToggle}\n            labels={resolvedLabels}\n            onToggleBlind={handleToggleBlind}\n            prompt={prompt}\n          />\n          <div className=\"grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-3\">\n            {children}\n          </div>\n        </section>\n      </ModelComparisonContext.Provider>\n    );\n  },\n);\nModelComparison.displayName = \"ModelComparison\";\n\n/**\n * Props for {@link ModelComparisonColumn}.\n *\n * @public\n */\nexport type ModelComparisonColumnProps = {\n  /** Optional badge text shown alongside the label (e.g. \"Winner\"). */\n  badge?: ReactNode;\n  /** Friendly display label for the model. Hidden in blind mode. */\n  label?: ReactNode;\n  /** Model identifier. Hidden in blind mode. */\n  model: string;\n} & ComponentPropsWithoutRef<\"article\">;\n\n/**\n * Column for {@link ModelComparison}. Renders the model label and badge in\n * the header and the response content in the body.\n *\n * @public\n */\nexport const ModelComparisonColumn = forwardRef<\n  HTMLElement,\n  ModelComparisonColumnProps\n>((props, ref) => {\n  const { badge, children, className, label, model, ...rest } = props;\n  const id = useId();\n  const { blind } = useContext(ModelComparisonContext);\n  const displayLabel = blind\n    ? `${HIDDEN_LABEL_PREFIX} ${id.slice(-2)}`\n    : (label ?? model);\n\n  return (\n    <article\n      aria-label={typeof displayLabel === \"string\" ? displayLabel : undefined}\n      className={cn(\n        \"flex flex-col gap-3 rounded-xl border border-border bg-muted/30 p-3\",\n        className,\n      )}\n      data-blind={blind ? \"true\" : \"false\"}\n      data-model={blind ? undefined : model}\n      ref={ref}\n      {...rest}\n    >\n      <header className=\"flex items-center justify-between gap-2\">\n        <h4 className=\"text-sm font-semibold tracking-tight text-foreground\">\n          {displayLabel}\n        </h4>\n        {badge ? <Badge variant=\"secondary\">{badge}</Badge> : null}\n      </header>\n      <div className=\"flex flex-1 flex-col gap-2 text-sm text-foreground\">\n        {children}\n      </div>\n    </article>\n  );\n});\nModelComparisonColumn.displayName = \"ModelComparisonColumn\";\n\n/**\n * Props for {@link ModelComparisonMeta}.\n *\n * @public\n */\nexport type ModelComparisonMetaProps = {\n  /** Cost stat (formatted). */\n  cost?: ReactNode;\n  /** Latency stat (formatted). */\n  latency?: ReactNode;\n  /** Token count. */\n  tokens?: number | ReactNode;\n} & ComponentPropsWithoutRef<\"dl\">;\n\n/**\n * Inline statistics row for a {@link ModelComparisonColumn}: tokens,\n * latency, cost. Renders the stats whose props are present.\n *\n * @public\n */\nexport const ModelComparisonMeta = forwardRef<\n  HTMLDListElement,\n  ModelComparisonMetaProps\n>((props, ref) => {\n  const { className, cost, latency, tokens, ...rest } = props;\n  const { labels } = useContext(ModelComparisonContext);\n\n  const items: { caption: string; value: ReactNode }[] = [];\n  if (tokens !== undefined && tokens !== null) {\n    items.push({\n      caption: labels.tokens,\n      value: typeof tokens === \"number\" ? tokens.toLocaleString() : tokens,\n    });\n  }\n  if (latency !== undefined && latency !== null) {\n    items.push({ caption: labels.latency, value: latency });\n  }\n  if (cost !== undefined && cost !== null) {\n    items.push({ caption: labels.cost, value: cost });\n  }\n  if (items.length === 0) return null;\n\n  return (\n    <dl\n      className={cn(\n        \"mt-auto flex flex-wrap gap-x-3 gap-y-1 border-t border-border pt-2 text-xs\",\n        className,\n      )}\n      ref={ref}\n      {...rest}\n    >\n      {items.map((item) => (\n        <div className=\"flex items-baseline gap-1\" key={item.caption}>\n          <dt className=\"font-medium uppercase tracking-wide text-muted-foreground\">\n            {item.caption}\n          </dt>\n          <dd className=\"text-foreground\">{item.value}</dd>\n        </div>\n      ))}\n    </dl>\n  );\n});\nModelComparisonMeta.displayName = \"ModelComparisonMeta\";\n\n/**\n * Vote payload shape passed to {@link ModelComparisonVoteProps.onVote}.\n *\n * @public\n */\nexport type ModelComparisonVoteValue = \"left\" | \"right\" | \"tie\";\n\ntype VoteLabels = {\n  left?: ReactNode;\n  right?: ReactNode;\n  tie?: ReactNode;\n};\n\n/**\n * Props for {@link ModelComparisonVote}.\n *\n * @public\n */\nexport type ModelComparisonVoteProps = {\n  /** Optional captions for the left / right / tie buttons. */\n  buttonLabels?: VoteLabels;\n  /** Fires with the user's choice. */\n  onVote?: (vote: ModelComparisonVoteValue) => void;\n} & ComponentPropsWithoutRef<\"div\">;\n\n/**\n * Vote bar for {@link ModelComparison}. Renders three buttons — left, tie,\n * right — that each emit `onVote` with the chosen value.\n *\n * @public\n */\nexport const ModelComparisonVote = forwardRef<\n  HTMLDivElement,\n  ModelComparisonVoteProps\n>((props, ref) => {\n  const { buttonLabels, className, onVote, ...rest } = props;\n  const { labels: contextLabels } = useContext(ModelComparisonContext);\n\n  const handleLeft = useCallback(() => {\n    onVote?.(\"left\");\n  }, [onVote]);\n  const handleTie = useCallback(() => {\n    onVote?.(\"tie\");\n  }, [onVote]);\n  const handleRight = useCallback(() => {\n    onVote?.(\"right\");\n  }, [onVote]);\n\n  return (\n    <div\n      className={cn(\"flex flex-col items-center gap-2\", className)}\n      ref={ref}\n      {...rest}\n    >\n      <p className=\"text-sm font-medium text-foreground\">\n        {contextLabels.voteHeading}\n      </p>\n      <div className=\"flex flex-wrap items-center justify-center gap-2\">\n        <Button onClick={handleLeft} size=\"sm\" type=\"button\" variant=\"outline\">\n          {buttonLabels?.left ?? \"← Left\"}\n        </Button>\n        <Button onClick={handleTie} size=\"sm\" type=\"button\" variant=\"ghost\">\n          {buttonLabels?.tie ?? contextLabels.voteTie}\n        </Button>\n        <Button onClick={handleRight} size=\"sm\" type=\"button\" variant=\"outline\">\n          {buttonLabels?.right ?? \"Right →\"}\n        </Button>\n      </div>\n    </div>\n  );\n});\nModelComparisonVote.displayName = \"ModelComparisonVote\";\n",
      "type": "registry:component"
    }
  ],
  "version": "0.2.1",
  "stability": "stable"
}
