> Premium standalone component (part of no design system, works in any React + Tailwind app) from AI Canvas. Index of every component and install setup: https://aicanvas.me/llms.txt

# Thinking Trace

A single steady line shows what an agent is thinking right now, then settles into how long it thought, with every step one click away.

- Category: AI Agent UI
- Built with: Motion, Tailwind CSS
- License: Premium (proprietary, not in the public repo)
- Page: https://aicanvas.me/components/reasoning-trace

## Install

```bash
npx shadcn@latest add @aicanvas/reasoning-trace
```

Requires Premium. Set your AI Canvas token as AICANVAS_TOKEN and register the @aicanvas registry with it, as described in the Install section of https://aicanvas.me/llms.txt. Without a Premium token the command writes a placeholder file instead of the component.

AI agents can also get its install command from the read-only AI Canvas MCP server (`npx -y @aicanvas/mcp`) and run it. Setup: https://aicanvas.me/mcp

Peer dependencies:

```bash
npm install framer-motion
```

## Props

### ThoughtStep

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `headline` | `string` |  | Summary shown for the reasoning step. |
| `body?` | `string` |  | Supporting explanation revealed when the trace is open; left out, only the headline is shown. |
| `sources?` | `string[]` |  | Source labels shown beneath the explanation; left out, no sources are shown for the step. |

### ReasoningTrace

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `steps?` | `ThoughtStep[]` |  | Supplies the reasoning timeline; left out, host mode has no steps, while passing it turns the demo off. |
| `status?` | `'thinking'\|'done'\|'stopped'` |  | Sets the trace state; left out, host mode is idle, while passing it turns the demo off. |
| `elapsedMs?` | `number` |  | Shows the thinking or stopped duration in milliseconds; left out, no duration is shown in host mode, while passing it turns the demo off. |
| `label?` | `string` |  | Adds a label beside the trace status; left out, no label is shown, while passing it turns the demo off. |
| `defaultOpen?` | `boolean` | `false` | Sets whether the reasoning steps start expanded. |
| `answer?` | `React.ReactNode` |  | Renders answer content below the trace whenever it is not thinking; left out, host mode shows no answer, while passing it turns the demo off. |
| `onToggle?` | `(open: boolean) => void` |  | Reports expansion changes while open state remains local; left out, expansion changes are not reported. |
| `onStop?` | `() => void` |  | Handles stopping and makes the stop control available while thinking; left out, host traces have no stop control. |
| `className?` | `string` |  | Adds classes to the outermost root; left out, only the built-in classes are used. |
| `tuning?` | `Partial<Tuning>` |  | Overrides trace measurements, timing and motion values; left out, every value in TUNING is used. |

## About

Reasoning Trace shows what an agent is thinking while it works. Thoughts stream in under phase headers such as Understand, Explore, Decide and Verify, the current phase shimmers and finished phases settle, and an honest timer ticks only while the agent thinks. Collapsed, it keeps the freshest thought visible on one line; expanded, long phases fold to their first lines. When the answer starts it becomes a one-line summary that stays expandable. Skip calls back to your code, keyboard toggles it, and it runs its own demo loop when rendered without props.

Use cases: AI coding agents, Research assistants, Chat assistants.

## Source

The source is Premium and is not published. Preview it at https://aicanvas.me/components/reasoning-trace.

## Remix prompt

The public part of the remix prompt: 1. Setup and 2. Constants. The rest of the build spec (state, markup, how it works) is part of Premium.

```markdown
# Reasoning Trace
An expandable AI reasoning card with a live headline ticker, step timeline, sources and final answer.

## 1. Setup
// npm install framer-motion
File: components-workspace/reasoning-trace/index.tsx  ·  'use client'  ·  export default function ReasoningTrace()
import { Fragment, useEffect, useId, useLayoutEffect, useRef, useState, useSyncExternalStore } from 'react'
import type * as React from 'react'
import { animate, AnimatePresence, motion, useIsPresent, useMotionValue, useTransform } from 'framer-motion'
import type { MotionValue } from 'framer-motion'
import { RotateCcw } from '@/app/_icons/icons/rotate-ccw'
import { ChevronDown } from '@/app/_icons/icons/chevron-down'
import { SparkleDuotone } from '@/app/_icons/icons/sparkle-duotone'
import { CircleStopDuotone } from '@/app/_icons/icons/circle-stop-duotone'
import { FileTextDuotone } from '@/app/_icons/icons/file-text-duotone'
import { GlobeDuotone } from '@/app/_icons/icons/globe-duotone'

Styling is Tailwind CSS v4 (the bg-(--var) and text-(color:--var) syntax, pointer-coarse: and class-based dark:). The @/app/_icons imports are the AI Canvas icon set; without it, use any icons that accept the same size, weight and className props.

## 2. Constants
export type Tuning = {
  cardMaxW: number
  rowH: number
  iconPx: number
  dotPx: number
  stepGapPx: number
  stopPx: number
  tickerMs: number
  tickerRisePx: number
  stateMs: number
  panelMs: number
  hoverMs: number
  lightMs: number
  answerDelayMs: number
  replayDelayMs: number
  wordFadeMs: number
  wordStepMs: number
  flowCapMs: number
  shimmerS: number
  pulseS: number
  pressScale: number
  revealShare: number
  dotPitch: number
  dotFieldH: number
  fieldRoomPx: number
  tickMs: number
  stopHoldMs: number
  firstThoughtMs: number
  thoughtEveryMs: number
  thinkEndMs: number
  demoSpeed: number
}

export const TUNING: Tuning = {
  cardMaxW: 480,
  rowH: 44,
  iconPx: 16,
  dotPx: 6,
  stepGapPx: 12,
  stopPx: 32,
  tickerMs: 220,
  tickerRisePx: 4,
  stateMs: 200,
  panelMs: 280,
  hoverMs: 150,
  lightMs: 600,
  answerDelayMs: 360,
  replayDelayMs: 700,
  wordFadeMs: 240,
  wordStepMs: 40,
  flowCapMs: 900,
  shimmerS: 2.6,
  pulseS: 1.2,
  pressScale: 0.97,
  revealShare: 0.75,
  dotPitch: 6,
  dotFieldH: 64,
  fieldRoomPx: 56,
  tickMs: 100,
  stopHoldMs: 5000,
  firstThoughtMs: 600,
  thoughtEveryMs: 2000,
  thinkEndMs: 8600,
  demoSpeed: 1,
}

const EASE = [0.23, 1, 0.32, 1] as const

// Keeps a number and its unit on one line ("4 steps").
const NBSP = String.fromCharCode(160)

export type ThoughtStep = {
  headline: string
  body?: string
  sources?: string[]
}

type Phase = 'thinking' | 'done' | 'stopped'

type View = Phase | 'idle'

type ReasoningTraceProps = {
  steps?: ThoughtStep[]
  status?: Phase
  elapsedMs?: number
  label?: string
  defaultOpen?: boolean
  answer?: React.ReactNode
  onToggle?: (open: boolean) => void
  onStop?: () => void
  className?: string
  tuning?: Partial<Tuning>
}

const STORY_STEPS: ThoughtStep[] = [
  {
    headline: 'Reading the failing test',
    body: 'The test expects 09:00 in Tokyo but gets 00:00. That is exactly the 9 hour offset.',
  },
  {
    headline: 'Looking for where dates are parsed',
    body: 'parseDate is called from three places; only the report path strips the offset.',
    sources: ['docs.example.dev', 'src/date.ts'],
  },
  {
    headline: 'Comparing two fixes',
    body: 'Converting at the edges would touch 12 files. Keeping the offset inside parseDate touches one.',
  },
  {
    headline: 'Checking the edge cases',
    body: 'Empty strings and dates without an offset still need to parse as UTC.',
  },
]

// Reduced motion as an external store: server and hydrating render both read false.
const REDUCED_QUERY = '(prefers-reduced-motion: reduce)'

function subscribeReduced(onChange: () => void) {
  if (typeof window === 'undefined' || !window.matchMedia) return () => {}
  const mq = window.matchMedia(REDUCED_QUERY)
  mq.addEventListener('change', onChange)
  return () => mq.removeEventListener('change', onChange)
}

function getReduced() {
  return typeof window !== 'undefined' && !!window.matchMedia && window.matchMedia(REDUCED_QUERY).matches
}

function getReducedServer() {
  return false
}

function fmtClock(ms: number) {
  const s = Math.floor(ms / 1000)
  return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`
}

function fmtThought(ms: number) {
  const s = Math.max(1, Math.round(ms / 1000))
  if (s < 60) return `${s}s`
  return `${Math.floor(s / 60)}m${NBSP}${String(s % 60).padStart(2, '0')}s`
}

function spokenThought(ms: number) {
  const s = Math.max(1, Math.round(ms / 1000))
  return `${s} second${s === 1 ? '' : 's'}`
}

const clamp01 = (v: number) => Math.min(1, Math.max(0, v))

const isWeb = (src: string) => /^[a-z0-9-]+(\.[a-z0-9-]+)+$/i.test(src.split('/')[0])

// Measured before paint and only once laid out, so a remount never animates from 0.
function useHeight<T extends HTMLElement>() {
  const ref = useRef<T>(null)
  const [h, setH] = useState<number | null>(null)
  useLayoutEffect(() => {
    const el = ref.current
    if (!el) return
    const read = () => {
      if (el.offsetWidth > 0) setH(el.offsetHeight)
    }
    read()
    const ro = new ResizeObserver(read)
    ro.observe(el)
    return () => ro.disconnect()
  }, [])
  return [ref, h] as const
}

const THEME_VARS = [
  '[--rt-page:#EEEEF3] dark:[--rt-page:#0E0E0F]',
  '[--rt-panel:#F4F4FA] dark:[--rt-panel:#1B1B1C]',
  '[--rt-well:#EEEEF3] dark:[--rt-well:#0E0E0F]',
  '[--rt-raised:#DFDFE3] dark:[--rt-raised:#2D2D2E]',
  '[--rt-raised-hover:#CACACD] dark:[--rt-raised-hover:#373738]',
  '[--rt-border:#DFDFE3] dark:[--rt-border:#2D2D2E]',
  '[--rt-border-strong:#CACACD] dark:[--rt-border-strong:#373738]',
  '[--rt-hairline:#DFDFE3] dark:[--rt-hairline:#2D2D2E]',
  '[--rt-rim:#CACACD] dark:[--rt-rim:#F4F4FA2E]',
  '[--rt-text:#1B1B1C] dark:[--rt-text:#F4F4FA]',
  '[--rt-body:#373738] dark:[--rt-body:#CACACD]',
  '[--rt-muted:#575759] dark:[--rt-muted:#9B9B9E]',
  '[--rt-faint:#7B7B7D]',
  '[--rt-selection:#A0A0A233] dark:[--rt-selection:#5E5E6333]',
  '[--rt-success:#047857] dark:[--rt-success:#34D399]',
  '[--rt-ai:#0369A1] dark:[--rt-ai:#7DD3FC]',
  '[--rt-attention:#B45309] dark:[--rt-attention:#FFB574]',
  '[--rt-red:#C8262C] dark:[--rt-red:#E5484D]',
  '[--rt-focus:#1B1B1C99] dark:[--rt-focus:#F4F4FA99]',
  '[--rt-focus-gap:#F4F4FA] dark:[--rt-focus-gap:#1B1B1C]',
  '[--rt-dot:#1B1B1C] dark:[--rt-dot:#F4F4FA]',
  '[--rt-elev-card:0_2px_6px_rgba(0,0,0,0.06),0_28px_56px_-16px_rgba(0,0,0,0.25)] dark:[--rt-elev-card:0_2px_6px_rgba(0,0,0,0.5),0_36px_72px_-18px_rgba(0,0,0,0.9)]',
].join(' ')

const CARD =
  'relative w-full rounded-[24px] border border-(color:--rt-border) bg-(--rt-panel) p-4 min-[400px]:p-5 shadow-(--rt-elev-card)'

const WELL = 'rounded-2xl border border-(color:--rt-border) bg-(--rt-well)'

const META = 'shrink-0 text-[12px] leading-4 tabular-nums text-(color:--rt-muted)'

const MONO = 'font-mono text-[12px] leading-[18px]'

const FOCUS =
  'outline-none focus-visible:ring-2 focus-visible:ring-(color:--rt-focus) focus-visible:ring-offset-2 focus-visible:ring-offset-(color:--rt-focus-gap)'

// Touch target via pseudo-element: 32px buttons reach 44px, the 32px Stop reaches 48px.
const HIT = "relative pointer-coarse:after:absolute pointer-coarse:after:content-['']"

const HIT_BTN = `${HIT} pointer-coarse:after:-inset-1.5`

const HIT_STOP = `${HIT} pointer-coarse:after:-inset-2`

const BTN =
  'inline-flex h-8 shrink-0 items-center justify-center gap-1.5 rounded-full px-4 text-[12px] leading-4 font-semibold transition-[background-color,color,border-color,box-shadow] duration-150 disabled:opacity-50'

const BTN_GHOST = `${BTN} text-(color:--rt-muted) hover:bg-(--rt-raised) hover:text-(color:--rt-text)`

type Tone = 'neutral' | 'ai' | 'attention' | 'success' | 'red'

const TONE_VAR: Record<Tone, string> = {
  neutral: 'var(--rt-dot)',
  ai: 'var(--rt-ai)',
  attention: 'var(--rt-attention)',
  success: 'var(--rt-success)',
  red: 'var(--rt-red)',
}

const TONE_STRENGTH: Record<Tone, number> = { neutral: 6, ai: 14, attention: 14, success: 10, red: 12 }

const SHIMMER_BG = 'linear-gradient(90deg, var(--rt-muted) 0%, var(--rt-text) 50%, var(--rt-muted) 100%)'

/**
 * @typedef {object} ThoughtStep
 * @property {string} headline Summary shown for the reasoning step.
 * @property {string} [body] Supporting explanation revealed when the trace is open; left out, only the headline is shown.
 * @property {string[]} [sources] Source labels shown beneath the explanation; left out, no sources are shown for the step.
 */

/**
 * @typedef {object} ReasoningTraceProps
 * @property {ThoughtStep[]} [steps] Supplies the reasoning timeline; left out, host mode has no steps, while passing it turns the demo off.
 * @property {'thinking'|'done'|'stopped'} [status] Sets the trace state; left out, host mode is idle, while passing it turns the demo off.
 * @property {number} [elapsedMs] Shows the thinking or stopped duration in milliseconds; left out, no duration is shown in host mode, while passing it turns the demo off.
 * @property {string} [label] Adds a label beside the trace status; left out, no label is shown, while passing it turns the demo off.
 * @property {boolean} [defaultOpen=false] Sets whether the reasoning steps start expanded.
 * @property {React.ReactNode} [answer] Renders answer content below the trace whenever it is not thinking; left out, host mode shows no answer, while passing it turns the demo off.
 * @property {(open: boolean) => void} [onToggle] Reports expansion changes while open state remains local; left out, expansion changes are not reported.
 * @property {() => void} [onStop] Handles stopping and makes the stop control available while thinking; left out, host traces have no stop control.
 * @property {string} [className] Adds classes to the outermost root; left out, only the built-in classes are used.
 * @property {Partial<Tuning>} [tuning] Overrides trace measurements, timing and motion values; left out, every value in TUNING is used.
 */
```

## Related

- [Agent Status](https://aicanvas.me/components/agent-status.md): A status pill that says what the agent is doing, whose move it is, time and cost, and opens to the step history and token bill.
- [Agent Composer](https://aicanvas.me/components/agent-composer.md): The message input an agent sits behind: a textarea that grows to six rows, inline slash commands, @file mentions as removable chips, a drag-and-drop attachment row, a model picker with each provider's logo, and a round send button whose glyph becomes a pulsing stop square while the agent works.
- [Checkpoint](https://aicanvas.me/components/agent-checkpoint.md): An agent checkpoint card that pauses for a decision on a question, command or plan, then collapses into a one-line receipt with undo, edit-before-allow and a risk grade.
- [Permission Request](https://aicanvas.me/components/permission-scope.md): A permission card where an agent asks for access and you allow it once, for this chat or always for a pattern, then revoke it from the receipt.
- [Code Block](https://aicanvas.me/components/code-panel.md): Code an agent just wrote streams in line by line, then numbered markers tie its notes to the exact lines, with Copy and Apply.
