{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "claude-thinking",
  "title": "Claude Thinking",
  "description": "The animated spinner and rotating verb Claude shows before replying.",
  "dependencies": [
    "cn"
  ],
  "registryDependencies": [
    "collapsible",
    "https://ui.usenotra.com/r/shimmer.json"
  ],
  "files": [
    {
      "path": "registry/notra/claude/components/claude-thinking.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"cn\";\nimport { useEffect, useState } from \"react\";\n\nimport { Shimmer } from \"../../shimmer/components/shimmer\";\nimport {\n  CLAUDE_THINKING_INTERVAL_MS,\n  CLAUDE_THINKING_VERBS,\n} from \"../constants/claude\";\nimport { useClaudeReducedMotion } from \"../hooks/use-claude-reduced-motion\";\nimport type { ClaudeThinkingProps } from \"../types/claude\";\nimport { ClaudeSpinner } from \"./claude-spinner\";\n\nexport const ClaudeThinking = ({\n  className,\n  intervalMs = CLAUDE_THINKING_INTERVAL_MS,\n  reducedMotion,\n  verbs = CLAUDE_THINKING_VERBS,\n  ...props\n}: ClaudeThinkingProps) => {\n  const reduced = useClaudeReducedMotion(reducedMotion);\n  const [verbIndex, setVerbIndex] = useState(0);\n  const verbCount = verbs.length;\n  const verb =\n    verbs[verbIndex % verbs.length] ?? verbs[0] ?? CLAUDE_THINKING_VERBS[0];\n\n  useEffect(() => {\n    if (reduced || verbCount <= 1) {\n      return;\n    }\n\n    const id = window.setInterval(() => {\n      setVerbIndex((current) => (current + 1) % verbCount);\n    }, intervalMs);\n\n    return () => window.clearInterval(id);\n  }, [intervalMs, reduced, verbCount]);\n\n  return (\n    <div\n      aria-live=\"polite\"\n      className={cn(\"text-claude-text flex items-center gap-3.5\", className)}\n      data-slot=\"claude-thinking\"\n      role=\"status\"\n      {...props}\n    >\n      <ClaudeSpinner\n        animated\n        aria-hidden=\"true\"\n        reducedMotion={reduced}\n        size={16}\n      />\n      <Shimmer\n        className=\"text-[0.9375rem] leading-6 [--shimmer-highlight:var(--claude-fg)]\"\n        paused={reduced}\n      >\n        {verb}\n      </Shimmer>\n    </div>\n  );\n};\n",
      "type": "registry:component",
      "target": "components/claude/components/claude-thinking.tsx"
    },
    {
      "path": "registry/notra/claude/components/claude-spinner.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"cn\";\nimport { useEffect, useState } from \"react\";\n\nimport {\n  CLAUDE_SPINNER_CENTER,\n  CLAUDE_SPINNER_FRAME_MS,\n  CLAUDE_SPINNER_FRAMES,\n  CLAUDE_SPINNER_SEQUENCE,\n  CLAUDE_SPINNER_STATIC_FRAME,\n  CLAUDE_SPINNER_VIEWBOX,\n} from \"../constants/claude\";\nimport { useClaudeReducedMotion } from \"../hooks/use-claude-reduced-motion\";\nimport type { ClaudeSpinnerFrame, ClaudeSpinnerProps } from \"../types/claude\";\n\nconst rayPoint = (angle: number, radius: number) => ({\n  x: CLAUDE_SPINNER_CENTER + Math.sin(angle) * radius,\n  y: CLAUDE_SPINNER_CENTER - Math.cos(angle) * radius,\n});\n\nconst frameRays = (frame: ClaudeSpinnerFrame) => {\n  const jitter = frame.jitter ?? 0;\n  return Array.from({ length: frame.rays }, (_, index) => {\n    const angle =\n      (Math.PI * 2 * index) / frame.rays + Math.sin(index * 2.35) * 0.07;\n    const length = frame.outer * (1 + Math.sin(index * 1.7) * jitter);\n    return {\n      end: rayPoint(angle, length),\n      id: `ray-${frame.rays}-${index}`,\n      start: rayPoint(angle, frame.inner),\n    };\n  });\n};\n\nconst ClaudeStarMark = ({ frame }: { frame: ClaudeSpinnerFrame }) => (\n  <svg\n    aria-hidden=\"true\"\n    className=\"size-full\"\n    fill=\"none\"\n    viewBox={`0 0 ${CLAUDE_SPINNER_VIEWBOX} ${CLAUDE_SPINNER_VIEWBOX}`}\n  >\n    {frame.rays === 0 ? (\n      <circle\n        cx={CLAUDE_SPINNER_CENTER}\n        cy={CLAUDE_SPINNER_CENTER}\n        fill=\"currentColor\"\n        r=\"1.7\"\n      />\n    ) : (\n      frameRays(frame).map((ray) => (\n        <line\n          key={ray.id}\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeWidth={frame.strokeWidth}\n          x1={ray.start.x}\n          x2={ray.end.x}\n          y1={ray.start.y}\n          y2={ray.end.y}\n        />\n      ))\n    )}\n  </svg>\n);\n\nexport const ClaudeSpinner = ({\n  \"aria-label\": ariaLabel = \"Loading\",\n  animated = false,\n  className,\n  reducedMotion,\n  size = 16,\n  style,\n  ...props\n}: ClaudeSpinnerProps) => {\n  const reduced = useClaudeReducedMotion(reducedMotion);\n  const shouldAnimate = animated && !reduced;\n  const [sequenceIndex, setSequenceIndex] = useState<number | null>(null);\n\n  useEffect(() => {\n    if (!shouldAnimate) {\n      setSequenceIndex(null);\n      return;\n    }\n\n    setSequenceIndex(0);\n    const id = window.setInterval(() => {\n      setSequenceIndex(\n        (current) => ((current ?? 0) + 1) % CLAUDE_SPINNER_SEQUENCE.length\n      );\n    }, CLAUDE_SPINNER_FRAME_MS);\n\n    return () => window.clearInterval(id);\n  }, [shouldAnimate]);\n\n  const frameIndex =\n    sequenceIndex === null\n      ? CLAUDE_SPINNER_STATIC_FRAME\n      : (CLAUDE_SPINNER_SEQUENCE[sequenceIndex] ?? CLAUDE_SPINNER_STATIC_FRAME);\n  const frame =\n    CLAUDE_SPINNER_FRAMES[frameIndex] ??\n    CLAUDE_SPINNER_FRAMES[CLAUDE_SPINNER_STATIC_FRAME];\n\n  return (\n    <span\n      className={cn(\n        \"text-claude-accent inline-flex shrink-0 items-center justify-center\",\n        className\n      )}\n      aria-label={ariaLabel}\n      data-slot=\"claude-spinner\"\n      role=\"status\"\n      style={{ height: size, width: size, ...style }}\n      {...props}\n    >\n      {frame && <ClaudeStarMark frame={frame} />}\n    </span>\n  );\n};\n",
      "type": "registry:component",
      "target": "components/claude/components/claude-spinner.tsx"
    },
    {
      "path": "registry/notra/claude/constants/claude.ts",
      "content": "import type { ClaudeSpinnerFrame } from \"../types/claude\";\n\nexport const CLAUDE_SEARCH_VERB_HOLD_MS = 1100;\n\nexport const CLAUDE_SEARCH_QUERY_MS = 420;\n\nexport const CLAUDE_SEARCH_RESULTS_MS = 640;\n\nexport const CLAUDE_SEARCH_STEP_MS = 380;\n\nexport const CLAUDE_SPINNER_FRAME_MS = 120;\n\nexport const CLAUDE_SPINNER_VIEWBOX = 24;\n\nexport const CLAUDE_SPINNER_CENTER = 12;\n\nexport const CLAUDE_SPINNER_FRAMES: readonly ClaudeSpinnerFrame[] = [\n  { inner: 0, outer: 0, rays: 0, strokeWidth: 0 },\n  { inner: 1.35, jitter: 0.08, outer: 4.4, rays: 4, strokeWidth: 1.55 },\n  { inner: 1.45, jitter: 0.1, outer: 5.3, rays: 8, strokeWidth: 1.45 },\n  { inner: 1.3, jitter: 0.06, outer: 6.5, rays: 6, strokeWidth: 1.55 },\n  { inner: 1.5, jitter: 0.12, outer: 6.9, rays: 8, strokeWidth: 1.5 },\n  { inner: 1.65, jitter: 0.16, outer: 7.5, rays: 10, strokeWidth: 1.4 },\n];\n\nconst SPINNER_FRAME_INDEXES = CLAUDE_SPINNER_FRAMES.map((_, index) => index);\n\nexport const CLAUDE_SPINNER_SEQUENCE: readonly number[] = [\n  ...SPINNER_FRAME_INDEXES,\n  ...[...SPINNER_FRAME_INDEXES].reverse().slice(1, -1),\n];\n\nexport const CLAUDE_SPINNER_STATIC_FRAME = CLAUDE_SPINNER_FRAMES.length - 1;\n\nexport const CLAUDE_THINKING_VERBS = [\n  \"Untangling\",\n  \"Pondering\",\n  \"Gathering\",\n  \"Sorting\",\n  \"Weaving\",\n] as const;\n\nexport const CLAUDE_THINKING_INTERVAL_MS = 2200;\n\nexport const CLAUDE_COPIED_RESET_MS = 1500;\n\nexport const CLAUDE_TOOLTIP_DELAY_MS = 200;\n\nexport const CLAUDE_MODEL_MENU_OPEN_DELAY_MS = 75;\n\nexport const CLAUDE_MODEL_MENU_CLOSE_DELAY_MS = 200;\n\nexport const CLAUDE_PLUS_MENU_SKILLS = [\"Docx\", \"PDF\", \"Spreadsheets\"] as const;\n\nexport const CLAUDE_PLUS_MENU_CONNECTORS = [\n  \"GitHub\",\n  \"Google Drive\",\n  \"Slack\",\n] as const;\n\nexport const CLAUDE_PLUS_MENU_DESIGN_SYSTEMS = [\n  \"Notra UI\",\n  \"shadcn/ui\",\n] as const;\n\nexport const CLAUDE_DEFAULT_CONTEXT_USAGE = 85;\n\nexport const CLAUDE_MENU_SURFACE_CLASS =\n  \"bg-claude-popover font-claude text-claude-fg ring-claude-popover-border rounded-2xl p-1.5 shadow-[0_4px_24px_var(--claude-popover-shadow)]\";\n\nexport const CLAUDE_MENU_ITEM_CLASS =\n  \"text-claude-fg focus:bg-claude-hover focus:text-claude-fg not-data-[variant=destructive]:focus:**:text-claude-fg data-highlighted:bg-claude-hover h-8 cursor-pointer gap-2.5 rounded-lg px-2.5 py-0 text-sm leading-5 [&_svg:not([class*='size-'])]:size-4.5\";\n\nexport const CLAUDE_HOVER_OPEN_DELAY_MS = 150;\n\nexport const CLAUDE_HOVER_CLOSE_DELAY_MS = 100;\n",
      "type": "registry:lib",
      "target": "components/claude/constants/claude.ts"
    },
    {
      "path": "registry/notra/claude/hooks/use-claude-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 useClaudeReducedMotion = (override?: boolean): boolean => {\n  const prefersReduced = useSyncExternalStore(\n    subscribe,\n    getSnapshot,\n    getServerSnapshot\n  );\n  return override ?? prefersReduced;\n};\n",
      "type": "registry:hook",
      "target": "components/claude/hooks/use-claude-reduced-motion.ts"
    },
    {
      "path": "registry/notra/claude/types/claude.ts",
      "content": "import type { ComponentProps, ReactNode } from \"react\";\n\nimport type { Collapsible } from \"@/components/ui/collapsible\";\n\nexport type ClaudeModelId =\n  | \"fable-5.1\"\n  | \"sonnet-5.5\"\n  | \"opus-5.5\"\n  | \"opus-5\"\n  | \"fable-5\"\n  | \"sonnet-5\"\n  | \"haiku-4.5\"\n  | \"opus-4.8\"\n  | \"opus-4.7\"\n  | \"opus-4.6\"\n  | \"opus-3\"\n  | \"sonnet-4.6\";\n\nexport type ClaudeEffortId = \"low\" | \"medium\" | \"high\" | \"extra\" | \"max\";\n\nexport interface ClaudeModelOption {\n  description: string;\n  group: \"latest\" | \"previous\";\n  id: ClaudeModelId;\n  label: string;\n}\n\nexport interface ClaudeEffortOption {\n  badge?: string;\n  id: ClaudeEffortId;\n  info?: string;\n  label: string;\n}\n\nexport type ClaudeMessageRole = \"user\" | \"assistant\";\n\nexport interface ClaudeSearchResult {\n  domain: string;\n  title: string;\n}\n\nexport interface ClaudeSearchGroup {\n  count: number;\n  query: string;\n  results: ClaudeSearchResult[];\n}\n\nexport interface ClaudeStepThought {\n  text: ReactNode;\n  type: \"thought\";\n}\n\nexport interface ClaudeStepTool {\n  code?: string;\n  content?: ReactNode;\n  count?: number;\n  detail?: ReactNode;\n  label: ReactNode;\n  logo?: ReactNode;\n  results?: ClaudeSearchResult[];\n  tool?: string;\n  type: \"tool\";\n}\n\nexport type ClaudeStepItem = ClaudeStepThought | ClaudeStepTool;\n\nexport type ClaudeSearchStepIcon = \"clock\" | \"check\" | \"error\";\n\nexport interface ClaudeSearchStep {\n  icon: ClaudeSearchStepIcon;\n  label: string;\n}\n\nexport interface ClaudeSourceItem {\n  domain: string;\n  favicon?: string;\n  href: string;\n  title: string;\n}\n\nexport interface ClaudeSpinnerFrame {\n  inner: number;\n  jitter?: number;\n  outer: number;\n  rays: number;\n  strokeWidth: number;\n}\n\nexport interface ClaudeSpinnerProps extends ComponentProps<\"span\"> {\n  animated?: boolean;\n  reducedMotion?: boolean;\n  size?: number;\n}\n\nexport interface ClaudeThinkingProps extends ComponentProps<\"div\"> {\n  intervalMs?: number;\n  reducedMotion?: boolean;\n  verbs?: readonly string[];\n}\n\nexport interface ClaudeSearchProps extends Omit<\n  ComponentProps<typeof Collapsible>,\n  \"children\"\n> {\n  groups?: readonly ClaudeSearchGroup[];\n  items?: readonly ClaudeStepItem[];\n  logo?: ReactNode;\n  queryLabel?: ReactNode;\n  reducedMotion?: boolean;\n  resultsLabel?: (count: number) => ReactNode;\n  sequential?: boolean;\n  steps?: readonly ClaudeSearchStep[];\n  summary?: ReactNode;\n  thought?: ReactNode;\n  verb?: ReactNode;\n}\n\nexport interface ClaudeSourcesProps extends ComponentProps<\"div\"> {\n  label?: ReactNode;\n  sources: readonly ClaudeSourceItem[];\n}\n\nexport interface ClaudeSourceLinkProps extends Omit<\n  ComponentProps<\"a\">,\n  \"href\"\n> {\n  source: ClaudeSourceItem;\n}\n\nexport interface ClaudeMessageProps extends ComponentProps<\"div\"> {\n  actions?: ReactNode;\n  from: ClaudeMessageRole;\n  search?: ReactNode;\n  sources?: ReactNode;\n}\n\nexport interface ClaudeActionsProps extends ComponentProps<\"div\"> {\n  from?: ClaudeMessageRole;\n  onBadResponse?: () => void;\n  onEdit?: () => void;\n  onGoodResponse?: () => void;\n  onReadAloud?: () => void;\n  onRetry?: () => void;\n  text: string;\n  timestamp?: ReactNode;\n}\n\nexport interface ClaudeActionButtonProps {\n  children: ReactNode;\n  label: string;\n  onClick?: () => void;\n}\n\nexport interface ClaudeModelSelectorProps {\n  className?: string;\n  effort: ClaudeEffortId;\n  effortLabel?: ReactNode;\n  model: ClaudeModelId;\n  moreModelsLabel?: ReactNode;\n  onEffortChange?: (effort: ClaudeEffortId) => void;\n  onModelChange?: (model: ClaudeModelId) => void;\n}\n\nexport interface ClaudeComposerProps extends Omit<\n  ComponentProps<\"div\">,\n  \"onSubmit\"\n> {\n  busy?: boolean;\n  defaultEffort?: ClaudeEffortId;\n  defaultModel?: ClaudeModelId;\n  disclaimer?: ReactNode;\n  effort?: ClaudeEffortId;\n  model?: ClaudeModelId;\n  onEffortChange?: (effort: ClaudeEffortId) => void;\n  onModelChange?: (model: ClaudeModelId) => void;\n  contextUsage?: number;\n  mode?: ReactNode;\n  onModeClick?: () => void;\n  onPlusSelect?: (item: ClaudePlusMenuItemId) => void;\n  onSend?: (text: string) => void;\n  onStop?: () => void;\n  placeholder?: string;\n}\n\nexport type ClaudePlusMenuItemId =\n  | \"add-files\"\n  | \"add-plugins\"\n  | \"research\"\n  | `skill:${string}`\n  | `connector:${string}`\n  | `design-system:${string}`;\n\nexport interface ClaudePlusMenuProps {\n  className?: string;\n  onSelect?: (item: ClaudePlusMenuItemId) => void;\n}\n\nexport interface ClaudeUsageRingProps extends ComponentProps<\"div\"> {\n  label?: string;\n  size?: number;\n  value: number;\n}\n\nexport interface ClaudeDemoSearch {\n  groups: ClaudeSearchGroup[];\n  items?: ClaudeStepItem[];\n  steps?: ClaudeSearchStep[];\n  summary?: string;\n  thought?: string;\n  verb: string;\n}\n\nexport interface ClaudeDemoMessage {\n  from: ClaudeMessageRole;\n  id: string;\n  search?: ClaudeDemoSearch;\n  sources?: ClaudeSourceItem[];\n  text: string;\n}\n\nexport interface ClaudeTextSegment {\n  offset: number;\n  text: string;\n}\n\nexport type ClaudePlaybackPhase =\n  | { status: \"idle\" }\n  | { status: \"playing\"; source: \"script\" | \"send\"; thinking: boolean };\n",
      "type": "registry:lib",
      "target": "components/claude/types/claude.ts"
    }
  ],
  "cssVars": {
    "theme": {
      "color-claude-bg": "var(--claude-bg)",
      "color-claude-fg": "var(--claude-fg)",
      "color-claude-border": "var(--claude-border)",
      "color-claude-muted": "var(--claude-muted)",
      "color-claude-subtle": "var(--claude-subtle)",
      "color-claude-faint": "var(--claude-faint)",
      "color-claude-hover": "var(--claude-hover)",
      "color-claude-bubble": "var(--claude-bubble)",
      "color-claude-input": "var(--claude-input)",
      "color-claude-input-border": "var(--claude-input-border)",
      "color-claude-input-shadow": "var(--claude-input-shadow)",
      "color-claude-send": "var(--claude-send)",
      "color-claude-send-hover": "var(--claude-send-hover)",
      "color-claude-send-fg": "var(--claude-send-fg)",
      "color-claude-accent": "var(--claude-accent)",
      "color-claude-check": "var(--claude-check)",
      "color-claude-rail": "var(--claude-rail)",
      "color-claude-text": "var(--claude-text)",
      "color-claude-meta": "var(--claude-meta)",
      "color-claude-card": "var(--claude-card)",
      "color-claude-card-border": "var(--claude-card-border)",
      "color-claude-card-divider": "var(--claude-card-divider)",
      "color-claude-step": "var(--claude-step)",
      "color-claude-danger": "var(--claude-danger)",
      "color-claude-pill": "var(--claude-pill)",
      "color-claude-pill-border": "var(--claude-pill-border)",
      "color-claude-pill-fg": "var(--claude-pill-fg)",
      "color-claude-popover": "var(--claude-popover)",
      "color-claude-popover-border": "var(--claude-popover-border)",
      "color-claude-popover-shadow": "var(--claude-popover-shadow)",
      "color-claude-tooltip": "var(--claude-tooltip)",
      "color-claude-tooltip-fg": "var(--claude-tooltip-fg)",
      "font-claude": "\"Anthropic Sans\", ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", sans-serif",
      "font-claude-serif": "\"Anthropic Serif\", ui-serif, Georgia, Cambria, \"Times New Roman\", Times, serif",
      "color-claude-usage": "var(--claude-usage)"
    },
    "light": {
      "claude-bg": "#fcfcfb",
      "claude-fg": "#0b0b0b",
      "claude-border": "#e3e3e2",
      "claude-muted": "#898781",
      "claude-subtle": "#898781",
      "claude-faint": "#9d9d9d",
      "claude-hover": "#e8e8e7",
      "claude-bubble": "#f0f0ef",
      "claude-input": "#ffffff",
      "claude-input-border": "#e0e0df",
      "claude-input-shadow": "rgb(0 0 0 / 3%)",
      "claude-send": "#0b0b0b",
      "claude-send-hover": "#000000",
      "claude-send-fg": "#ffffff",
      "claude-accent": "#d97757",
      "claude-check": "#2c84db",
      "claude-rail": "#e3e3e2",
      "claude-text": "#52514e",
      "claude-meta": "#898781",
      "claude-card": "#f0f0ef",
      "claude-card-border": "#e3e3e2",
      "claude-card-divider": "#e3e3e2",
      "claude-step": "#52514e",
      "claude-danger": "#b53333",
      "claude-pill": "#f0f0ef",
      "claude-pill-border": "rgb(0 0 0 / 6%)",
      "claude-pill-fg": "#52514e",
      "claude-popover": "#ffffff",
      "claude-popover-border": "rgb(0 0 0 / 8%)",
      "claude-popover-shadow": "rgb(0 0 0 / 10%)",
      "claude-tooltip": "#0b0b0b",
      "claude-tooltip-fg": "#fcfcfb",
      "claude-usage": "#009300"
    },
    "dark": {
      "claude-bg": "#262624",
      "claude-fg": "#faf9f5",
      "claude-border": "#3d3d3a",
      "claude-muted": "#9c9a92",
      "claude-subtle": "#9c9a92",
      "claude-faint": "#85837c",
      "claude-hover": "#3a3a37",
      "claude-bubble": "#141413",
      "claude-input": "#30302e",
      "claude-input-border": "#44433f",
      "claude-input-shadow": "transparent",
      "claude-send": "#faf9f5",
      "claude-send-hover": "#ffffff",
      "claude-send-fg": "#141413",
      "claude-accent": "#d97757",
      "claude-check": "#5a9ee6",
      "claude-rail": "#3d3d3a",
      "claude-text": "#c2c0b6",
      "claude-meta": "#9c9a92",
      "claude-card": "#1f1e1d",
      "claude-card-border": "#3d3d3a",
      "claude-card-divider": "#3d3d3a",
      "claude-step": "#c2c0b6",
      "claude-danger": "#e5776b",
      "claude-pill": "#1f1e1d",
      "claude-pill-border": "rgb(255 255 255 / 8%)",
      "claude-pill-fg": "#c2c0b6",
      "claude-popover": "#30302e",
      "claude-popover-border": "rgb(222 220 209 / 15%)",
      "claude-popover-shadow": "rgb(0 0 0 / 40%)",
      "claude-tooltip": "#faf9f5",
      "claude-tooltip-fg": "#141413",
      "claude-usage": "#3fb950"
    }
  },
  "type": "registry:component"
}