{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "gemini-thoughts",
  "title": "Gemini Thoughts",
  "description": "The \"Show thinking\" step with thoughts and searches.",
  "dependencies": [
    "cn",
    "lucide-react"
  ],
  "registryDependencies": [
    "badge",
    "button",
    "collapsible"
  ],
  "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-thoughts.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"cn\";\nimport { ChevronDownIcon, SearchIcon } from \"lucide-react\";\n\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Collapsible,\n  CollapsibleContent,\n  CollapsibleTrigger,\n} from \"@/components/ui/collapsible\";\n\nimport {\n  GEMINI_HIDE_THINKING_LABEL,\n  GEMINI_SHOW_THINKING_LABEL,\n} from \"../constants/gemini\";\nimport type { GeminiThoughtsProps } from \"../types/gemini\";\n\nconst withOccurrenceKeys = <T,>(\n  items: readonly T[],\n  getKey: (item: T) => string\n) => {\n  const seen = new Map<string, number>();\n  return items.map((item) => {\n    const base = getKey(item);\n    const count = (seen.get(base) ?? 0) + 1;\n    seen.set(base, count);\n    return [item, `${base}#${count}`] as const;\n  });\n};\n\nexport const GeminiThoughts = ({\n  className,\n  hideLabel = GEMINI_HIDE_THINKING_LABEL,\n  showLabel = GEMINI_SHOW_THINKING_LABEL,\n  steps,\n  ...props\n}: GeminiThoughtsProps) => (\n  <Collapsible\n    className={cn(\"group/gemini-thoughts font-gemini w-full\", className)}\n    data-slot=\"gemini-thoughts\"\n    {...props}\n  >\n    <CollapsibleTrigger\n      render={\n        <Button\n          className=\"text-gemini-muted hover:bg-gemini-hover hover:text-gemini-fg focus-visible:ring-gemini-focus/30 aria-expanded:text-gemini-fg dark:hover:bg-gemini-hover -ms-2 h-8 gap-2 rounded-full border-0 px-2 text-[0.9375rem] leading-5 font-normal focus-visible:ring-2 [&[data-panel-open]>svg:last-child]:rotate-180\"\n          variant=\"ghost\"\n        />\n      }\n    >\n      <span className=\"group-data-open/gemini-thoughts:hidden\">\n        {showLabel}\n      </span>\n      <span className=\"hidden group-data-open/gemini-thoughts:inline\">\n        {hideLabel}\n      </span>\n      <ChevronDownIcon\n        aria-hidden=\"true\"\n        className=\"size-4 transition-transform duration-200 motion-reduce:transition-none\"\n        strokeWidth={1.5}\n      />\n    </CollapsibleTrigger>\n    <CollapsibleContent className=\"h-(--collapsible-panel-height) overflow-hidden transition-[height,opacity] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] data-ending-style:h-0 data-ending-style:opacity-0 data-starting-style:h-0 data-starting-style:opacity-0 motion-reduce:transition-none\">\n      <div className=\"border-gemini-separator text-gemini-muted mt-2 flex flex-col gap-3 border-s-2 ps-4 text-[0.875rem] leading-6\">\n        {withOccurrenceKeys(steps, (item) =>\n          item.kind === \"thought\" ? item.text : item.queries.join(\"|\")\n        ).map(([step, key]) =>\n          step.kind === \"thought\" ? (\n            <p key={key}>{step.text}</p>\n          ) : (\n            <div className=\"flex flex-col gap-2\" key={key}>\n              <span className=\"text-gemini-fg flex items-center gap-2 text-[0.8125rem] leading-5\">\n                <SearchIcon\n                  aria-hidden=\"true\"\n                  className=\"size-3.5\"\n                  strokeWidth={1.75}\n                />\n                Searching the web\n              </span>\n              <span className=\"flex flex-wrap gap-1.5\">\n                {withOccurrenceKeys(step.queries, (query) => query).map(\n                  ([query, queryKey]) => (\n                    <Badge\n                      className=\"border-gemini-separator text-gemini-muted h-auto rounded-full bg-transparent px-2.5 py-0.5 text-[0.8125rem] leading-5 font-normal\"\n                      key={queryKey}\n                      variant=\"outline\"\n                    >\n                      {query}\n                    </Badge>\n                  )\n                )}\n              </span>\n            </div>\n          )\n        )}\n      </div>\n    </CollapsibleContent>\n  </Collapsible>\n);\n",
      "type": "registry:component",
      "target": "components/gemini/components/gemini-thoughts.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"
    },
    {
      "path": "registry/notra/gemini/components/gemini-sparkle.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"cn\";\nimport type { CSSProperties } from \"react\";\nimport { useEffect, useState } from \"react\";\n\nimport { GEMINI_SPARKLE_CYCLE_MS } from \"../constants/gemini\";\nimport { useGeminiReducedMotion } from \"../hooks/use-gemini-reduced-motion\";\nimport type { GeminiSparkleProps } from \"../types/gemini\";\n\nconst VIEW = 24;\nconst DOT_R = 2.15;\nconst BOUNCE_END = 0.4;\nconst MORPH_IN_END = 0.56;\nconst HOLD_END = 0.7;\nconst MORPH_OUT_END = 0.86;\nconst BOUNCE_HEIGHT = 2.4;\nconst BOUNCE_GROWTH = 0.08;\nconst BOUNCES_PER_CYCLE = 5;\n\ninterface Point {\n  x: number;\n  y: number;\n}\n\nconst LINE: readonly Point[] = [\n  { x: 6, y: 13.2 },\n  { x: 12, y: 13.2 },\n  { x: 18, y: 13.2 },\n];\n\nconst TRIANGLE: readonly Point[] = [\n  { x: 7.2, y: 15.8 },\n  { x: 12, y: 6.8 },\n  { x: 16.8, y: 15.8 },\n];\n\nconst lerp = (from: number, to: number, t: number) => from + (to - from) * t;\n\nconst easeInOut = (t: number) =>\n  t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2;\n\nconst morphAmount = (progress: number) => {\n  if (progress < BOUNCE_END) {\n    return 0;\n  }\n  if (progress < MORPH_IN_END) {\n    return easeInOut((progress - BOUNCE_END) / (MORPH_IN_END - BOUNCE_END));\n  }\n  if (progress < HOLD_END) {\n    return 1;\n  }\n  if (progress < MORPH_OUT_END) {\n    return 1 - easeInOut((progress - HOLD_END) / (MORPH_OUT_END - HOLD_END));\n  }\n  return 0;\n};\n\nconst bounceLift = (progress: number, index: number, morph: number) => {\n  const wave = Math.max(\n    0,\n    Math.sin((progress * BOUNCES_PER_CYCLE - index / 3) * Math.PI * 2)\n  );\n  const rest = 1 - morph;\n  return wave * wave * rest * rest;\n};\n\nconst GeminiSparkleMark = ({ progress }: { progress: number }) => {\n  const morph = morphAmount(progress);\n\n  return (\n    <svg\n      aria-hidden=\"true\"\n      className=\"size-full\"\n      fill=\"currentColor\"\n      viewBox={`0 0 ${VIEW} ${VIEW}`}\n    >\n      {LINE.map((from, index) => {\n        const to = TRIANGLE[index] ?? from;\n        const bounce = bounceLift(progress, index, morph);\n        return (\n          <circle\n            cx={lerp(from.x, to.x, morph)}\n            cy={lerp(from.y, to.y, morph) - bounce * BOUNCE_HEIGHT}\n            key={from.x}\n            r={DOT_R * (1 + bounce * BOUNCE_GROWTH)}\n          />\n        );\n      })}\n    </svg>\n  );\n};\n\nexport const GeminiSparkle = ({\n  animated = false,\n  className,\n  reducedMotion,\n  size = 18,\n  style,\n  ...props\n}: GeminiSparkleProps) => {\n  const prefersReducedMotion = useGeminiReducedMotion();\n  const shouldAnimate = animated && !(reducedMotion ?? prefersReducedMotion);\n  const [progress, setProgress] = useState(0);\n\n  useEffect(() => {\n    if (!shouldAnimate) {\n      setProgress(0);\n      return;\n    }\n\n    const started = performance.now();\n    let frame = 0;\n\n    const tick = (now: number) => {\n      setProgress(\n        ((now - started) % GEMINI_SPARKLE_CYCLE_MS) / GEMINI_SPARKLE_CYCLE_MS\n      );\n      frame = window.requestAnimationFrame(tick);\n    };\n\n    frame = window.requestAnimationFrame(tick);\n    return () => {\n      window.cancelAnimationFrame(frame);\n    };\n  }, [shouldAnimate]);\n\n  return (\n    <span\n      className={cn(\n        \"text-gemini-fg inline-flex size-(--gemini-sparkle-size) shrink-0 items-center justify-center\",\n        className\n      )}\n      aria-hidden={animated ? undefined : true}\n      aria-label={animated ? \"Loading\" : undefined}\n      data-animated={shouldAnimate || undefined}\n      data-slot=\"gemini-sparkle\"\n      role={animated ? \"status\" : undefined}\n      style={\n        { \"--gemini-sparkle-size\": `${size}px`, ...style } as CSSProperties\n      }\n      {...props}\n    >\n      <GeminiSparkleMark progress={shouldAnimate ? progress : 0} />\n    </span>\n  );\n};\n",
      "type": "registry:component",
      "target": "components/gemini/components/gemini-sparkle.tsx"
    },
    {
      "path": "registry/notra/gemini/hooks/use-gemini-reduced-motion.ts",
      "content": "\"use client\";\n\nimport { useSyncExternalStore } from \"react\";\n\nconst REDUCED_MOTION_QUERY = \"(prefers-reduced-motion: reduce)\";\n\nconst subscribe = (onChange: () => void) => {\n  const query = window.matchMedia(REDUCED_MOTION_QUERY);\n  query.addEventListener(\"change\", onChange);\n  return () => query.removeEventListener(\"change\", onChange);\n};\n\nconst getSnapshot = () => window.matchMedia(REDUCED_MOTION_QUERY).matches;\n\nconst getServerSnapshot = () => false;\n\nexport const useGeminiReducedMotion = (): boolean =>\n  useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n",
      "type": "registry:hook",
      "target": "components/gemini/hooks/use-gemini-reduced-motion.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"
}