{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "gemini-source-chip",
  "title": "Gemini Source Chip",
  "description": "An inline source pill with a hover card.",
  "dependencies": [
    "cn"
  ],
  "registryDependencies": [
    "avatar",
    "badge",
    "button",
    "collapsible",
    "hover-card"
  ],
  "files": [
    {
      "path": "registry/notra/gemini/types/gemini.ts",
      "content": "import type { ComponentProps, ReactNode } from \"react\";\n\nimport type { Button } from \"@/components/ui/button\";\nimport type { Collapsible } from \"@/components/ui/collapsible\";\n\nexport type GeminiModelId = \"flash-lite\" | \"flash\" | \"pro\" | \"thinking\";\n\nexport type GeminiModelGroup = \"core\" | \"thinking\";\n\nexport interface GeminiModelOption {\n  badge?: string;\n  chip: string;\n  description: string;\n  group: GeminiModelGroup;\n  id: GeminiModelId;\n  label: string;\n}\n\nexport type GeminiMessageRole = \"user\" | \"assistant\";\n\nexport interface GeminiMessageProps extends ComponentProps<\"div\"> {\n  actions?: ReactNode;\n  from: GeminiMessageRole;\n  status?: ReactNode;\n  thoughts?: ReactNode;\n}\n\nexport interface GeminiActionButtonProps extends ComponentProps<typeof Button> {\n  label: string;\n}\n\nexport interface GeminiActionsProps extends ComponentProps<\"div\"> {\n  onBadResponse?: () => void;\n  onGoodResponse?: () => void;\n}\n\nexport interface GeminiSparkleProps extends ComponentProps<\"span\"> {\n  animated?: boolean;\n  reducedMotion?: boolean;\n  size?: number;\n}\n\nexport interface GeminiThinkingProps extends ComponentProps<\"div\"> {\n  label?: ReactNode;\n  reducedMotion?: boolean;\n}\n\nexport interface GeminiModelSelectorProps extends Omit<\n  ComponentProps<typeof Button>,\n  \"defaultValue\" | \"onChange\" | \"value\"\n> {\n  defaultModel?: GeminiModelId;\n  model?: GeminiModelId;\n  models?: readonly GeminiModelOption[];\n  onModelChange?: (model: GeminiModelId) => void;\n}\n\nexport interface GeminiComposerProps extends Omit<\n  ComponentProps<\"div\">,\n  \"onSubmit\"\n> {\n  busy?: boolean;\n  defaultModel?: GeminiModelId;\n  disclaimer?: ReactNode;\n  model?: GeminiModelId;\n  models?: readonly GeminiModelOption[];\n  onModelChange?: (model: GeminiModelId) => void;\n  onSend?: (text: string) => void;\n  onStop?: () => void;\n  placeholder?: string;\n  privacyHref?: string;\n  privacyLabel?: ReactNode;\n}\n\nexport interface GeminiSource {\n  description?: string;\n  domain: string;\n  favicon?: string;\n  href?: string;\n  id: string;\n  name: string;\n  title: string;\n}\n\nexport type GeminiThoughtStep =\n  | { kind: \"search\"; queries: readonly string[] }\n  | { kind: \"thought\"; text: string };\n\nexport interface GeminiSourceChipProps extends Omit<\n  ComponentProps<\"a\">,\n  \"children\" | \"href\"\n> {\n  source: GeminiSource;\n}\n\nexport interface GeminiThoughtsProps extends Omit<\n  ComponentProps<typeof Collapsible>,\n  \"children\"\n> {\n  hideLabel?: ReactNode;\n  showLabel?: ReactNode;\n  steps: readonly GeminiThoughtStep[];\n}\n\nexport interface GeminiStoryTextProps extends Omit<\n  ComponentProps<\"div\">,\n  \"children\"\n> {\n  sources?: readonly GeminiSource[];\n  text: string;\n}\n\nexport interface GeminiStoryMessage {\n  from: GeminiMessageRole;\n  id: string;\n  search?: boolean;\n  sources?: readonly GeminiSource[];\n  text: string;\n  thoughts?: readonly GeminiThoughtStep[];\n}\n\nexport interface GeminiTextSegment {\n  offset: number;\n  text: string;\n}\n\nexport type GeminiPlaybackPhase =\n  | { status: \"idle\" }\n  | { status: \"playing\"; source: \"script\" | \"send\"; thinking: boolean };\n",
      "type": "registry:lib",
      "target": "components/gemini/types/gemini.ts"
    },
    {
      "path": "registry/notra/gemini/lib/gemini-text.ts",
      "content": "import type { GeminiTextSegment } from \"../types/gemini\";\n\nconst BOLD_PATTERN = /(\\*\\*[^*]+\\*\\*)/g;\n\nconst CITE_PATTERN = /(\\{\\{[a-z0-9-]+\\}\\})/g;\n\nconst PARAGRAPH_SEPARATOR = \"\\n\\n\";\n\nexport const splitWithOffsets = (\n  text: string,\n  separator: string | RegExp,\n  separatorLength: number\n): GeminiTextSegment[] => {\n  const segments: GeminiTextSegment[] = [];\n  let offset = 0;\n  for (const part of text.split(separator)) {\n    if (part.length > 0) {\n      segments.push({ offset, text: part });\n    }\n    offset += part.length + separatorLength;\n  }\n  return segments;\n};\n\nexport const splitGeminiParagraphs = (text: string): GeminiTextSegment[] =>\n  splitWithOffsets(text, PARAGRAPH_SEPARATOR, PARAGRAPH_SEPARATOR.length);\n\nexport const splitGeminiBold = (text: string): GeminiTextSegment[] =>\n  splitWithOffsets(text, BOLD_PATTERN, 0);\n\nexport const splitGeminiCitations = (text: string): GeminiTextSegment[] =>\n  splitWithOffsets(text, CITE_PATTERN, 0);\n\nexport const geminiCitationId = (text: string): string | null =>\n  text.match(/^\\{\\{([a-z0-9-]+)\\}\\}$/)?.[1] ?? null;\n",
      "type": "registry:lib",
      "target": "components/gemini/lib/gemini-text.ts"
    },
    {
      "path": "registry/notra/gemini/constants/gemini-story.ts",
      "content": "import type {\n  GeminiSource,\n  GeminiStoryMessage,\n  GeminiThoughtStep,\n} from \"../types/gemini\";\n\nexport const GEMINI_STORY_SOURCES: GeminiSource[] = [\n  {\n    description:\n      \"Cursor's cloud agents run on remote machines, so a task keeps going after you close the laptop, and you can pick it up from the editor again.\",\n    domain: \"cursor.com\",\n    href: \"https://cursor.com\",\n    id: \"cursor-blog\",\n    name: \"Cursor\",\n    title: \"Cloud agents: work that keeps running without your laptop\",\n  },\n  {\n    description:\n      \"Write-ahead log segments are shipped to object storage, so state stays durable and versioned while the hot path stays off the editor's thread.\",\n    domain: \"aws.amazon.com\",\n    href: \"https://aws.amazon.com/s3\",\n    id: \"s3-docs\",\n    name: \"AWS\",\n    title: \"Amazon S3 versioning and durable storage\",\n  },\n];\n\nexport const GEMINI_STORY_THOUGHTS: GeminiThoughtStep[] = [\n  {\n    kind: \"thought\",\n    text: \"The user asks about two terms. I should check what each one means before answering.\",\n  },\n  {\n    kind: \"search\",\n    queries: [\"cursor origin cloud workspace\", \"wal-on-s3 snapshots artifacts\"],\n  },\n  {\n    kind: \"thought\",\n    text: \"Both results agree: Origin is the remote workspace and wal-on-s3 is where its state is stored.\",\n  },\n];\n\nexport const GEMINI_STORY_THREAD: GeminiStoryMessage[] = [\n  {\n    from: \"user\",\n    id: \"u-1\",\n    text: \"what are cursor origin and wal-on-s3?\",\n  },\n  {\n    from: \"assistant\",\n    id: \"a-1\",\n    search: true,\n    sources: GEMINI_STORY_SOURCES,\n    text: \"**Cursor Origin** is Cursor's cloud workspace: the agent keeps working on your repo remotely, even with your laptop closed. {{cursor-blog}}\\n\\n**wal-on-s3** stores its snapshots and artifacts on S3, so files stay versioned and nothing blocks the editor. {{s3-docs}}\",\n    thoughts: GEMINI_STORY_THOUGHTS,\n  },\n];\n\nexport const GEMINI_STORY_REPLIES = [\n  \"Got it. Let me know when you want to keep going.\",\n  \"Sure. I'm here.\",\n  \"Noted. What's next?\",\n] as const;\n\nexport const GEMINI_STORY_ASSISTANT_MESSAGES = GEMINI_STORY_THREAD.filter(\n  (message) => message.from === \"assistant\"\n);\n",
      "type": "registry:lib",
      "target": "components/gemini/constants/gemini-story.ts"
    },
    {
      "path": "registry/notra/gemini/components/gemini-source-chip.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"cn\";\n\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\";\nimport { badgeVariants } from \"@/components/ui/badge\";\nimport {\n  HoverCard,\n  HoverCardContent,\n  HoverCardTrigger,\n} from \"@/components/ui/hover-card\";\n\nimport {\n  GEMINI_FAVICON_URL,\n  GEMINI_HOVER_CLOSE_DELAY_MS,\n  GEMINI_HOVER_OPEN_DELAY_MS,\n} from \"../constants/gemini\";\nimport type { GeminiSourceChipProps } from \"../types/gemini\";\n\nexport const GeminiSourceChip = ({\n  className,\n  rel = \"noopener noreferrer\",\n  source,\n  target = \"_blank\",\n  ...props\n}: GeminiSourceChipProps) => (\n  <HoverCard>\n    <HoverCardTrigger\n      className={cn(\n        badgeVariants({ variant: \"secondary\" }),\n        \"bg-gemini-bubble text-gemini-muted hover:bg-gemini-hover hover:text-gemini-fg focus-visible:outline-gemini-focus mx-1 h-6 rounded-full border-0 px-2.5 align-baseline text-[0.8125rem] leading-none font-normal no-underline focus-visible:ring-0 focus-visible:outline-2 focus-visible:outline-solid motion-reduce:transition-none\",\n        className\n      )}\n      closeDelay={GEMINI_HOVER_CLOSE_DELAY_MS}\n      data-slot=\"gemini-source-chip\"\n      delay={GEMINI_HOVER_OPEN_DELAY_MS}\n      href={source.href}\n      rel={rel}\n      target={target}\n      {...props}\n    >\n      {source.name}\n    </HoverCardTrigger>\n    <HoverCardContent\n      align=\"start\"\n      className=\"bg-gemini-popover font-gemini text-gemini-fg shadow-gemini-popover ring-gemini-border w-80 max-w-[calc(100vw-2rem)] gap-0 rounded-2xl p-4\"\n      sideOffset={8}\n    >\n      <div className=\"text-gemini-muted flex items-center gap-2 text-sm leading-5\">\n        <Avatar className=\"size-4 rounded-sm after:hidden\">\n          <AvatarImage\n            alt=\"\"\n            className=\"rounded-sm\"\n            src={\n              source.favicon ??\n              `${GEMINI_FAVICON_URL}?domain=${encodeURIComponent(source.domain)}&sz=64`\n            }\n          />\n          <AvatarFallback className=\"bg-gemini-hover rounded-sm\" />\n        </Avatar>\n        {source.name}\n      </div>\n      <p className=\"mt-2 text-[0.9375rem] leading-5\">{source.title}</p>\n      {source.description ? (\n        <p className=\"text-gemini-muted mt-2 line-clamp-2 text-[0.8125rem] leading-[1.15rem]\">\n          {source.description}\n        </p>\n      ) : null}\n    </HoverCardContent>\n  </HoverCard>\n);\n",
      "type": "registry:component",
      "target": "components/gemini/components/gemini-source-chip.tsx"
    },
    {
      "path": "registry/notra/gemini/constants/gemini.ts",
      "content": "export const GEMINI_SPARKLE_CYCLE_MS = 3400;\n\nexport const GEMINI_TOOLTIP_DELAY_MS = 200;\n\nexport const GEMINI_SEARCHING_LABEL = \"Searching the web\";\n\nexport const GEMINI_THINKING_LABEL = \"Thinking\";\n\nexport const GEMINI_HOVER_OPEN_DELAY_MS = 150;\n\nexport const GEMINI_HOVER_CLOSE_DELAY_MS = 120;\n\nexport const GEMINI_FAVICON_URL = \"https://www.google.com/s2/favicons\";\n\nexport const GEMINI_SHOW_THINKING_LABEL = \"Show thinking\";\n\nexport const GEMINI_HIDE_THINKING_LABEL = \"Hide thinking\";\n",
      "type": "registry:lib",
      "target": "components/gemini/constants/gemini.ts"
    }
  ],
  "cssVars": {
    "theme": {
      "color-gemini-bg": "var(--gemini-bg)",
      "color-gemini-fg": "var(--gemini-fg)",
      "color-gemini-muted": "var(--gemini-muted)",
      "color-gemini-subtle": "var(--gemini-subtle)",
      "color-gemini-border": "var(--gemini-border)",
      "color-gemini-hover": "var(--gemini-hover)",
      "color-gemini-bubble": "var(--gemini-bubble)",
      "color-gemini-composer": "var(--gemini-composer)",
      "color-gemini-composer-border": "var(--gemini-composer-border)",
      "color-gemini-chip-fg": "var(--gemini-chip-fg)",
      "color-gemini-badge-fg": "var(--gemini-badge-fg)",
      "color-gemini-send": "var(--gemini-send)",
      "color-gemini-send-fg": "var(--gemini-send-fg)",
      "color-gemini-send-hover": "var(--gemini-send-hover)",
      "color-gemini-stop": "var(--gemini-stop)",
      "color-gemini-stop-hover": "var(--gemini-stop-hover)",
      "color-gemini-separator": "var(--gemini-separator)",
      "color-gemini-focus": "var(--gemini-focus)",
      "color-gemini-popover": "var(--gemini-popover)",
      "color-gemini-tooltip": "var(--gemini-tooltip)",
      "color-gemini-tooltip-fg": "var(--gemini-tooltip-fg)",
      "shadow-gemini-composer": "var(--gemini-composer-shadow)",
      "shadow-gemini-popover": "var(--gemini-popover-shadow)",
      "font-gemini": "\"Google Sans Flex\", \"Google Sans\", Roboto, Arial, sans-serif",
      "color-gemini-placeholder": "var(--gemini-placeholder)"
    },
    "light": {
      "gemini-bg": "#ffffff",
      "gemini-fg": "#1f1f1f",
      "gemini-muted": "#444746",
      "gemini-subtle": "#80868b",
      "gemini-border": "#00000014",
      "gemini-hover": "#f1f3f4",
      "gemini-bubble": "#f0f0f0",
      "gemini-composer": "#ffffff",
      "gemini-composer-border": "#0000000a",
      "gemini-composer-shadow": "0 1px 2px rgb(60 64 67 / 0.08), 0 2px 12px rgb(60 64 67 / 0.08)",
      "gemini-chip-fg": "#3c4043",
      "gemini-badge-fg": "#3c4043",
      "gemini-send": "#1f1f1f",
      "gemini-send-fg": "#ffffff",
      "gemini-send-hover": "#000000",
      "gemini-stop": "#e8eaed",
      "gemini-stop-hover": "#dadce0",
      "gemini-separator": "#e8eaed",
      "gemini-focus": "#1a73e8",
      "gemini-popover": "#ffffff",
      "gemini-popover-shadow": "0 8px 28px rgb(60 64 67 / 0.16)",
      "gemini-tooltip": "#171717",
      "gemini-tooltip-fg": "#fafafa",
      "gemini-placeholder": "#5f6368"
    },
    "dark": {
      "gemini-bg": "#1f1f1f",
      "gemini-fg": "#e3e3e3",
      "gemini-muted": "#c4c7c5",
      "gemini-subtle": "#80868b",
      "gemini-border": "#ffffff1a",
      "gemini-hover": "#ffffff1a",
      "gemini-bubble": "#ffffff1a",
      "gemini-composer": "#ffffff0f",
      "gemini-composer-border": "#ffffff1a",
      "gemini-composer-shadow": "0 1px 2px rgb(0 0 0 / 0.24)",
      "gemini-chip-fg": "#fafafa",
      "gemini-badge-fg": "#e8eaed",
      "gemini-send": "#ffffff",
      "gemini-send-fg": "#1f1f1f",
      "gemini-send-hover": "#ffffffe6",
      "gemini-stop": "#ffffff1f",
      "gemini-stop-hover": "#ffffff29",
      "gemini-separator": "#ffffff1a",
      "gemini-focus": "#1a73e8",
      "gemini-popover": "#131419",
      "gemini-popover-shadow": "0 8px 28px rgb(0 0 0 / 0.45)",
      "gemini-tooltip": "#fafafa",
      "gemini-tooltip-fg": "#171717",
      "gemini-placeholder": "#bdc1c6"
    }
  },
  "type": "registry:component"
}