{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "gemini-thinking",
  "title": "Gemini Thinking",
  "description": "The animated Gemini sparkle with a shimmering status label.",
  "dependencies": [
    "cn"
  ],
  "registryDependencies": [
    "button",
    "collapsible",
    "https://ui.usenotra.com/r/shimmer.json"
  ],
  "files": [
    {
      "path": "registry/notra/gemini/components/gemini-thinking.tsx",
      "content": "import { cn } from \"cn\";\n\nimport { Shimmer } from \"../../shimmer/components/shimmer\";\nimport { GEMINI_SEARCHING_LABEL } from \"../constants/gemini\";\nimport type { GeminiThinkingProps } from \"../types/gemini\";\nimport { GeminiSparkle } from \"./gemini-sparkle\";\n\nexport const GeminiThinking = ({\n  className,\n  label = GEMINI_SEARCHING_LABEL,\n  reducedMotion,\n  ...props\n}: GeminiThinkingProps) => (\n  <div\n    aria-live=\"polite\"\n    className={cn(\n      \"font-gemini text-gemini-fg flex items-center gap-2.5 text-[0.9375rem] leading-none\",\n      className\n    )}\n    data-slot=\"gemini-thinking\"\n    role=\"status\"\n    {...props}\n  >\n    <GeminiSparkle\n      animated\n      aria-hidden=\"true\"\n      aria-label={undefined}\n      reducedMotion={reducedMotion}\n      role=\"presentation\"\n      size={20}\n    />\n    <Shimmer\n      className=\"[--shimmer-highlight:var(--gemini-subtle)]\"\n      paused={reducedMotion}\n    >\n      {label}\n    </Shimmer>\n  </div>\n);\n",
      "type": "registry:component",
      "target": "components/gemini/components/gemini-thinking.tsx"
    },
    {
      "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/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/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"
    },
    {
      "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"
    }
  ],
  "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"
}