{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "run-06",
  "title": "Context cells",
  "description": "The context as a field of cells, one per thousand tokens, filling in order as turns arrive and lighting up part by part.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://ui.ndekocode.dev/r/agent-run.json",
    "https://ui.ndekocode.dev/r/demonstration.json",
    "https://ui.ndekocode.dev/r/hatch.json",
    "https://ui.ndekocode.dev/r/icons.json",
    "https://ui.ndekocode.dev/r/live-duration.json",
    "https://ui.ndekocode.dev/r/live-region.json",
    "https://ui.ndekocode.dev/r/morph-button.json",
    "https://ui.ndekocode.dev/r/motion-number.json",
    "https://ui.ndekocode.dev/r/motion-presets.json",
    "https://ui.ndekocode.dev/r/motion-word-roll.json",
    "https://ui.ndekocode.dev/r/replay-button.json",
    "https://ui.ndekocode.dev/r/theme.json",
    "https://ui.ndekocode.dev/r/use-kept-height.json",
    "https://ui.ndekocode.dev/r/use-prefers-reduced-motion.json",
    "https://ui.ndekocode.dev/r/use-session.json",
    "https://ui.ndekocode.dev/r/use-timeouts.json",
    "https://ui.ndekocode.dev/r/utils.json"
  ],
  "files": [
    {
      "path": "registry/ndeko/components/run/run-06.tsx",
      "content": "'use client';\n\nimport { useId, useState } from 'react';\n\nimport { useKeptHeight } from '@/registry/ndeko/hooks/use-kept-height';\nimport { usePrefersReducedMotion } from '@/registry/ndeko/hooks/use-prefers-reduced-motion';\nimport { formatTokens } from '@/registry/ndeko/lib/agent-run';\nimport { spokenUnless } from '@/registry/ndeko/lib/demonstration';\nimport { cn } from '@/registry/ndeko/lib/utils';\n\nimport { CompactOffer, contextAnnouncementOf } from './_parts/compact-offer';\nimport {\n  freeTokensOf,\n  LEVEL_WORD,\n  levelOf,\n  PART_FILL,\n  RESERVE_STYLE,\n  usedTokensOf,\n  type Compaction,\n  type ContextBudget,\n  type ContextItem,\n  type ContextSlot,\n} from './_parts/context';\nimport { ContextHeader } from './_parts/context-header';\nimport { ClockGate } from '@/registry/ndeko/ui/live-duration';\nimport { LiveRegion } from '@/registry/ndeko/ui/live-region';\nimport { ReplayButton } from '@/registry/ndeko/ui/replay-button';\nimport { TokenCount } from './_parts/token-count';\nimport {\n  FILLING_SCRIPT,\n  FILLING_SESSION,\n  useContextSession,\n  type CompactStatus,\n  type ContextSession,\n  type ContextStep,\n} from './_parts/use-context-demo';\n\ntype CellKind = ContextSlot;\n\ntype LegendEntry = {\n  kind: CellKind;\n  label: string;\n  tokens: number;\n};\n\n/** One cell holds a thousand tokens; a 200k context is a field of 10 rows of 20. */\nconst TOKENS_PER_CELL = 1000;\n\nconst FILL_STAGGER_MS = 14;\n\nconst FREE_FILL = 'color-mix(in oklab, var(--foreground) 6%, transparent)';\n\nfunction fillOf(kind: Exclude<CellKind, 'reserve'>) {\n  if (kind === 'free') {\n    return FREE_FILL;\n  }\n  return PART_FILL[kind];\n}\n\nfunction cellStyleOf(kind: CellKind): React.CSSProperties {\n  if (kind === 'reserve') {\n    return RESERVE_STYLE;\n  }\n  return { backgroundColor: fillOf(kind) };\n}\n\nconst DIMMED_PERCENT = 25;\n\nconst DIMMED_OPACITY = DIMMED_PERCENT / 100;\n\n/**\n * A cell that steps back keeps a quarter of its colour. The colour itself fades, so two hundred\n * cells dim in one repaint of the field and none of them becomes a layer of its own.\n */\nfunction dimmedCellStyleOf(kind: CellKind): React.CSSProperties {\n  if (kind === 'reserve') {\n    return { ...RESERVE_STYLE, opacity: DIMMED_OPACITY };\n  }\n  const fill = fillOf(kind);\n  return {\n    backgroundColor: `color-mix(in oklab, ${fill} ${DIMMED_PERCENT}%, transparent)`,\n  };\n}\n\nfunction cellCountOf(tokens: number) {\n  return Math.round(tokens / TOKENS_PER_CELL);\n}\n\n/** Parts fill cells in order, rounded on running totals so the field always adds up to the limit. */\nfunction cellKindsOf(budget: ContextBudget) {\n  const total = cellCountOf(budget.limit);\n  const reserveCells = cellCountOf(budget.reserve);\n  const freeEnd = total - reserveCells;\n  const kinds: CellKind[] = [];\n  let runningTokens = 0;\n  for (const part of budget.parts) {\n    runningTokens += part.tokens;\n    const partEnd = cellCountOf(runningTokens);\n    const boundary = Math.min(partEnd, freeEnd);\n    while (kinds.length < boundary) {\n      kinds.push(part.id);\n    }\n  }\n  while (kinds.length < freeEnd) {\n    kinds.push('free');\n  }\n  while (kinds.length < total) {\n    kinds.push('reserve');\n  }\n  return kinds;\n}\n\nfunction isPartCell(kind: CellKind) {\n  const isFree = kind === 'free';\n  const isReserve = kind === 'reserve';\n  return !isFree && !isReserve;\n}\n\nfunction filledCountOf(kinds: CellKind[]) {\n  const filled = kinds.filter(isPartCell);\n  return filled.length;\n}\n\ntype FillHistory = { filled: number; previous: number };\n\nfunction delayOf(index: number, history: FillHistory, isStaggered: boolean) {\n  if (!isStaggered) {\n    return 0;\n  }\n  const isNew = index >= history.previous;\n  const isFilled = index < history.filled;\n  if (isNew && isFilled) {\n    const order = index - history.previous;\n    return order * FILL_STAGGER_MS;\n  }\n  return 0;\n}\n\nfunction isDimmedCell(kind: CellKind, activeKind: CellKind | null) {\n  if (activeKind === null) {\n    return false;\n  }\n  return kind !== activeKind;\n}\n\nfunction useFillHistory(filled: number) {\n  const [history, setHistory] = useState<FillHistory>({\n    filled,\n    previous: filled,\n  });\n  if (history.filled !== filled) {\n    setHistory({ filled, previous: history.filled });\n  }\n  return history;\n}\n\nfunction legendOf(budget: ContextBudget): LegendEntry[] {\n  const entries: LegendEntry[] = [];\n  for (const part of budget.parts) {\n    if (part.tokens > 0) {\n      entries.push({ kind: part.id, label: part.label, tokens: part.tokens });\n    }\n  }\n  const free = freeTokensOf(budget);\n  entries.push({ kind: 'free', label: 'Free', tokens: free });\n  entries.push({\n    kind: 'reserve',\n    label: 'Held for the answer',\n    tokens: budget.reserve,\n  });\n  return entries;\n}\n\nfunction spokenFieldOf(budget: ContextBudget) {\n  const used = usedTokensOf(budget);\n  const usedText = formatTokens(used);\n  const limitText = formatTokens(budget.limit);\n  return `${usedText} of ${limitText} tokens used, one cell per thousand`;\n}\n\nfunction cellColorOf(kind: CellKind, isDimmed: boolean) {\n  if (isDimmed) {\n    return dimmedCellStyleOf(kind);\n  }\n  return cellStyleOf(kind);\n}\n\nfunction cellLookOf(\n  kind: CellKind,\n  index: number,\n  history: FillHistory,\n  isStaggered: boolean,\n  isDimmed: boolean\n): React.CSSProperties {\n  const delay = delayOf(index, history, isStaggered);\n  const color = cellColorOf(kind, isDimmed);\n  return { ...color, transitionDelay: `${delay}ms` };\n}\n\nfunction CellField({\n  budget,\n  activeKind,\n}: {\n  budget: ContextBudget;\n  activeKind: CellKind | null;\n}) {\n  const shouldReduceMotion = usePrefersReducedMotion();\n  const kinds = cellKindsOf(budget);\n  const filled = filledCountOf(kinds);\n  const history = useFillHistory(filled);\n  const isStaggered = !shouldReduceMotion;\n  const spoken = spokenFieldOf(budget);\n\n  return (\n    <div role='img' aria-label={spoken} className='grid grid-cols-20 gap-0.5'>\n      {kinds.map((kind, index) => (\n        <span\n          key={index}\n          className='aspect-square rounded-[3px] transition-[background-color,opacity] duration-300 ease-(--ease-out-strong)'\n          style={cellLookOf(\n            kind,\n            index,\n            history,\n            isStaggered,\n            isDimmedCell(kind, activeKind)\n          )}\n        />\n      ))}\n    </div>\n  );\n}\n\nfunction litRowOf(isLit: boolean) {\n  if (isLit) {\n    return 'bg-foreground/[0.03]';\n  }\n  return undefined;\n}\n\nfunction LegendButton({\n  entry,\n  isPinned,\n  isLit,\n  onPreview,\n  onTogglePin,\n}: {\n  entry: LegendEntry;\n  isPinned: boolean;\n  /** Its cells are the ones lit now, whoever pointed at them. */\n  isLit: boolean;\n  onPreview: (kind: CellKind | null) => void;\n  onTogglePin: (kind: CellKind) => void;\n}) {\n  function preview() {\n    onPreview(entry.kind);\n  }\n\n  function clearPreview() {\n    onPreview(null);\n  }\n\n  return (\n    <button\n      type='button'\n      aria-pressed={isPinned}\n      onPointerEnter={preview}\n      onPointerLeave={clearPreview}\n      onFocus={preview}\n      onBlur={clearPreview}\n      onClick={() => onTogglePin(entry.kind)}\n      className={cn(\n        'hover:bg-foreground/[0.03] focus-visible:ring-ring/25 aria-pressed:bg-foreground/[0.05] flex h-8 w-full cursor-pointer items-center gap-2.5 rounded-md px-2 text-start text-[13px] transition-colors duration-150 outline-none focus-visible:ring-[3px]',\n        litRowOf(isLit)\n      )}\n    >\n      <span\n        aria-hidden='true'\n        className='size-2.5 shrink-0 rounded-[3px]'\n        style={cellStyleOf(entry.kind)}\n      />\n      <span className='min-w-0 flex-1 truncate'>{entry.label}</span>\n      <TokenCount tokens={entry.tokens} className='text-muted-foreground' />\n    </button>\n  );\n}\n\nfunction barStyleOf(turn: ContextItem, maxTokens: number): React.CSSProperties {\n  const share = turn.tokens / maxTokens;\n  const height = Math.max(8, share * 100);\n  return { height: `${height}%` };\n}\n\nfunction turnCountOf(count: number) {\n  if (count === 1) {\n    return '1 turn';\n  }\n  return `${count} turns`;\n}\n\nfunction maxTokensOf(turns: ContextItem[]) {\n  let max = 1;\n  for (const turn of turns) {\n    max = Math.max(max, turn.tokens);\n  }\n  return max;\n}\n\nfunction turnFillOf(index: number, lastIndex: number, foldedTurns: number) {\n  if (index < foldedTurns) {\n    return 'bg-chart-4';\n  }\n  if (index === lastIndex) {\n    return 'bg-chart-1';\n  }\n  return 'bg-foreground/20';\n}\n\nfunction TurnSparkline({\n  turns,\n  foldedTurns,\n}: {\n  turns: ContextItem[];\n  foldedTurns: number;\n}) {\n  const maxTokens = maxTokensOf(turns);\n  const lastIndex = turns.length - 1;\n\n  return (\n    <div className='flex flex-col gap-1.5'>\n      <div className='flex items-baseline justify-between gap-3 text-xs'>\n        <span className='text-muted-foreground'>Tokens added per turn</span>\n        <span className='text-muted-foreground tabular-nums'>\n          {turnCountOf(turns.length)}\n        </span>\n      </div>\n      <div aria-hidden='true' className='flex h-8 items-end gap-0.5'>\n        {turns.map((turn, index) => (\n          <span\n            key={turn.id}\n            className={cn(\n              'min-w-0 flex-1 origin-bottom rounded-t-[2px] transition-[background-color,scale] duration-300 ease-(--ease-out-strong) starting:scale-y-0',\n              turnFillOf(index, lastIndex, foldedTurns)\n            )}\n            style={barStyleOf(turn, maxTokens)}\n          />\n        ))}\n      </div>\n      <div className='sr-only'>\n        <table>\n          <caption>Tokens added per turn</caption>\n          <tbody>\n            {turns.map((turn) => (\n              <tr key={turn.id}>\n                <th scope='row'>{turn.label}</th>\n                <td>{formatTokens(turn.tokens)}</td>\n              </tr>\n            ))}\n          </tbody>\n        </table>\n      </div>\n    </div>\n  );\n}\n\n/** What a person points at comes first, then what they pinned, then what was lit from outside. */\nfunction activeKindOf(\n  preview: CellKind | null,\n  pinned: CellKind | null,\n  spotlight: CellKind | null\n) {\n  if (preview !== null) {\n    return preview;\n  }\n  if (pinned !== null) {\n    return pinned;\n  }\n  return spotlight;\n}\n\nexport type ContextCellsProps = {\n  budget: ContextBudget;\n  turns: ContextItem[];\n  compaction?: Compaction | null;\n  canCompact?: boolean;\n  compactStatus?: CompactStatus;\n  onCompact?: () => void;\n  /** Lights the cells of one part from outside, for a demonstration or a control beside the field. */\n  spotlight?: ContextSlot | null;\n  /** Shows Compact held down, for a demonstration that presses it. */\n  isCompactPressed?: boolean;\n  className?: string;\n};\n\nexport function ContextCells({\n  budget,\n  turns,\n  compaction = null,\n  canCompact = false,\n  compactStatus = 'idle',\n  onCompact,\n  spotlight = null,\n  isCompactPressed = false,\n  className,\n}: ContextCellsProps) {\n  const [previewKind, setPreviewKind] = useState<CellKind | null>(null);\n  const [pinnedKind, setPinnedKind] = useState<CellKind | null>(null);\n  const titleId = useId();\n  const legend = legendOf(budget);\n  const activeKind = activeKindOf(previewKind, pinnedKind, spotlight);\n  const foldedTurns = compaction?.foldedTurns ?? 0;\n\n  function togglePin(kind: CellKind) {\n    setPinnedKind((current) => {\n      if (current === kind) {\n        return null;\n      }\n      return kind;\n    });\n  }\n\n  return (\n    <section\n      aria-labelledby={titleId}\n      className={cn(\n        'bg-card hairline-ring ring-border @container flex flex-col gap-4 rounded-xl p-4 shadow-[0_1px_2px_rgb(0_0_0/0.04)]',\n        className\n      )}\n    >\n      <ContextHeader budget={budget} titleId={titleId} />\n      <CellField budget={budget} activeKind={activeKind} />\n      <ul\n        aria-label='Parts of the context, point at one to light its cells'\n        className='-mx-2 grid grid-cols-1 gap-x-3 @md:grid-cols-2'\n      >\n        {legend.map((entry) => (\n          <li key={entry.kind}>\n            <LegendButton\n              entry={entry}\n              isPinned={pinnedKind === entry.kind}\n              isLit={activeKind === entry.kind}\n              onPreview={setPreviewKind}\n              onTogglePin={togglePin}\n            />\n          </li>\n        ))}\n      </ul>\n      <TurnSparkline turns={turns} foldedTurns={foldedTurns} />\n      <CompactOffer\n        budget={budget}\n        canCompact={canCompact}\n        compactStatus={compactStatus}\n        compaction={compaction}\n        onCompact={onCompact}\n        isPressed={isCompactPressed}\n      />\n    </section>\n  );\n}\n\nfunction spot(after: number, slot: ContextSlot | null): ContextStep {\n  return { after, by: 'demo', action: { type: 'spotlight', slot } };\n}\n\n/** The demonstration points at each part in turn before the conversation goes on. */\nconst TOUR: ContextStep[] = [\n  spot(1200, 'system'),\n  spot(650, 'tools'),\n  spot(650, 'files'),\n  spot(650, 'conversation'),\n  spot(650, 'free'),\n  spot(900, null),\n];\n\nconst SESSION: ContextSession = {\n  ...FILLING_SESSION,\n  script: [...TOUR, ...FILLING_SCRIPT],\n};\n\n/** A part lit by the demonstration lets go the moment a person takes over. */\nfunction spotlightOf(isAutoplaying: boolean, spotlight: ContextSlot | null) {\n  if (isAutoplaying) {\n    return spotlight;\n  }\n  return null;\n}\n\nexport default function Run06() {\n  const {\n    ref,\n    isInView,\n    isAutoplaying,\n    canReplay,\n    pressed,\n    turns,\n    budget,\n    compaction,\n    compactStatus,\n    canCompact,\n    spotlight,\n    compact,\n    replay,\n  } = useContextSession(SESSION);\n  const level = levelOf(budget);\n  const announcement = contextAnnouncementOf(\n    level,\n    LEVEL_WORD[level],\n    compaction\n  );\n\n  useKeptHeight(ref);\n\n  return (\n    <div\n      ref={ref}\n      className='flex min-h-[42.5rem] w-full max-w-[28rem] flex-col gap-2'\n    >\n      <ClockGate isActive={isInView}>\n        <ContextCells\n          budget={budget}\n          turns={turns}\n          compaction={compaction}\n          canCompact={canCompact}\n          compactStatus={compactStatus}\n          onCompact={compact}\n          spotlight={spotlightOf(isAutoplaying, spotlight)}\n          isCompactPressed={pressed === 'compact'}\n        />\n      </ClockGate>\n      <div className='flex h-7 items-center justify-end'>\n        <ReplayButton\n          isShown={canReplay}\n          onReplay={replay}\n          label='Replay the conversation'\n        />\n      </div>\n      <LiveRegion message={spokenUnless(isAutoplaying, announcement)} />\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/run/run-06.tsx"
    },
    {
      "path": "registry/ndeko/components/run/_parts/compact-offer.tsx",
      "content": "'use client';\n\nimport { AnimatePresence, motion } from 'motion/react';\n\nimport { usePrefersReducedMotion } from '@/registry/ndeko/hooks/use-prefers-reduced-motion';\nimport { formatTokens } from '@/registry/ndeko/lib/agent-run';\nimport { pressedSolidOf } from '@/registry/ndeko/lib/demonstration';\nimport { CheckIcon } from '@/registry/ndeko/lib/icons';\n\nimport {\n  compactionLineOf,\n  freeTokensOf,\n  levelOf,\n  type Compaction,\n  type ContextBudget,\n  type ContextLevel,\n} from './context';\nimport { MorphButton, type MorphState } from '@/registry/ndeko/ui/morph-button';\nimport { risePresenceOf } from '@/registry/ndeko/lib/motion-presets';\nimport type { CompactStatus } from './use-context-demo';\n\nconst COMPACT_LABELS: Record<MorphState, string> = {\n  idle: 'Compact',\n  busy: 'Compacting',\n  done: 'Compacted',\n};\n\nfunction morphStateOf(status: CompactStatus): MorphState {\n  if (status === 'compacting') {\n    return 'busy';\n  }\n  return 'idle';\n}\n\nfunction isOfferShown(\n  level: ContextLevel,\n  canCompact: boolean,\n  status: CompactStatus\n) {\n  const isCompactSoon = level === 'compact';\n  const isCompacting = status === 'compacting';\n  const isAvailable = canCompact || isCompacting;\n  return isCompactSoon && isAvailable;\n}\n\nfunction traceOf(compaction: Compaction | null, isOffered: boolean) {\n  if (isOffered) {\n    return null;\n  }\n  return compaction;\n}\n\ntype CompactOfferProps = {\n  budget: ContextBudget;\n  canCompact: boolean;\n  compactStatus: CompactStatus;\n  compaction: Compaction | null;\n  onCompact?: () => void;\n  /** Shows Compact held down, for a demonstration that presses it. */\n  isPressed?: boolean;\n};\n\nexport function CompactOffer({\n  budget,\n  canCompact,\n  compactStatus,\n  compaction,\n  onCompact,\n  isPressed = false,\n}: CompactOfferProps) {\n  const shouldReduceMotion = usePrefersReducedMotion();\n  const risePresence = risePresenceOf(shouldReduceMotion);\n  const level = levelOf(budget);\n  const free = freeTokensOf(budget);\n  const freeText = formatTokens(free);\n  const isOffered = isOfferShown(level, canCompact, compactStatus);\n  const trace = traceOf(compaction, isOffered);\n\n  return (\n    <AnimatePresence initial={false} mode='popLayout'>\n      {isOffered && (\n        <motion.div key='offer' className='@container' {...risePresence}>\n          <div className='bg-warning/10 flex flex-col gap-2.5 rounded-lg p-3 @lg:flex-row @lg:items-center'>\n            <p className='text-warning-foreground min-w-0 flex-1 text-xs leading-5'>\n              {freeText} tokens left before the reserve. Fold the older turns\n              into a summary to keep going.\n            </p>\n            <MorphButton\n              type='button'\n              size='sm'\n              state={morphStateOf(compactStatus)}\n              labels={COMPACT_LABELS}\n              onClick={onCompact}\n              className={pressedSolidOf(isPressed)}\n            />\n          </div>\n        </motion.div>\n      )}\n      {trace && (\n        <motion.p\n          key='trace'\n          className='text-muted-foreground flex items-center gap-2 text-xs'\n          {...risePresence}\n        >\n          <CheckIcon className='text-success-foreground size-3.5' />\n          {compactionLineOf(trace)}\n        </motion.p>\n      )}\n    </AnimatePresence>\n  );\n}\n\nexport function contextAnnouncementOf(\n  level: ContextLevel,\n  levelWord: string,\n  compaction: Compaction | null\n) {\n  if (compaction) {\n    const line = compactionLineOf(compaction);\n    return `${line}. Context: ${levelWord}`;\n  }\n  if (level === 'compact') {\n    return `Context: ${levelWord}. Compact is available`;\n  }\n  return `Context: ${levelWord}`;\n}\n",
      "type": "registry:component",
      "target": "components/run/_parts/compact-offer.tsx"
    },
    {
      "path": "registry/ndeko/components/run/_parts/context-header.tsx",
      "content": "'use client';\n\nimport { WordRoll } from '@/components/core/word-roll';\nimport { cn } from '@/registry/ndeko/lib/utils';\n\nimport {\n  LEVEL_LABELS,\n  LEVEL_TONE,\n  LEVEL_WORD,\n  levelOf,\n  usedTokensOf,\n  type ContextBudget,\n} from './context';\nimport { TokenCount } from './token-count';\n\ntype ContextHeaderProps = {\n  budget: ContextBudget;\n  titleId: string;\n  title?: string;\n};\n\nexport function ContextHeader({\n  budget,\n  titleId,\n  title = 'Context',\n}: ContextHeaderProps) {\n  const used = usedTokensOf(budget);\n  const level = levelOf(budget);\n\n  return (\n    <header className='flex items-start justify-between gap-3'>\n      <div className='min-w-0'>\n        <h3 id={titleId} className='text-sm font-medium'>\n          {title}\n        </h3>\n        <p\n          className={cn(\n            'text-xs transition-colors duration-200',\n            LEVEL_TONE[level]\n          )}\n        >\n          <WordRoll text={LEVEL_WORD[level]} labels={LEVEL_LABELS} />\n        </p>\n      </div>\n      <p className='text-[13px] tabular-nums'>\n        <TokenCount tokens={used} className='font-medium' />\n        <span className='text-muted-foreground'>\n          {' of '}\n          <TokenCount tokens={budget.limit} />\n        </span>\n      </p>\n    </header>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/run/_parts/context-header.tsx"
    },
    {
      "path": "registry/ndeko/components/run/_parts/context.ts",
      "content": "import { hatchOf } from '@/registry/ndeko/lib/hatch';\n\nexport type ContextPartId =\n  'system' | 'tools' | 'files' | 'summary' | 'conversation';\n\n/** A place in the window: one of its parts, the room left, or what is held back for the answer. */\nexport type ContextSlot = ContextPartId | 'free' | 'reserve';\n\nexport type ContextItem = {\n  id: string;\n  label: string;\n  tokens: number;\n};\n\nexport type ContextPart = {\n  id: ContextPartId;\n  label: string;\n  tokens: number;\n  items: ContextItem[];\n};\n\nexport type ContextBudget = {\n  limit: number;\n  /** Held back for the model's answer: never filled by the context. */\n  reserve: number;\n  parts: ContextPart[];\n};\n\nexport type ContextLevel = 'room' | 'filling' | 'compact';\n\nexport type Compaction = {\n  foldedTurns: number;\n  summaryTokens: number;\n};\n\nexport const CONTEXT_LIMIT = 200_000;\n\nexport const CONTEXT_RESERVE = 16_000;\n\n/** Parts are parts of one whole, not entities: they take the chart ramp and a muted fill, never the series. */\nexport const PART_FILL: Record<ContextPartId, string> = {\n  system: 'color-mix(in oklab, var(--muted-foreground) 30%, transparent)',\n  tools: 'var(--chart-3)',\n  files: 'var(--chart-2)',\n  summary: 'var(--chart-4)',\n  conversation: 'var(--chart-1)',\n};\n\nexport const RESERVE_STYLE = {\n  backgroundColor:\n    'color-mix(in oklab, var(--muted-foreground) 8%, transparent)',\n  backgroundImage: hatchOf('var(--muted-foreground)', 40),\n};\n\nexport const LEVEL_WORD: Record<ContextLevel, string> = {\n  room: 'Plenty of room',\n  filling: 'Filling up',\n  compact: 'Compact soon',\n};\n\nexport const LEVEL_LABELS = [\n  LEVEL_WORD.room,\n  LEVEL_WORD.filling,\n  LEVEL_WORD.compact,\n];\n\nexport const LEVEL_TONE: Record<ContextLevel, string> = {\n  room: 'text-muted-foreground',\n  filling: 'text-foreground',\n  compact: 'text-warning-foreground',\n};\n\nconst FILLING_FROM = 0.6;\n\nconst COMPACT_FROM = 0.85;\n\n/** The newest turns stay word for word after a compaction. */\nconst KEPT_TURNS = 2;\n\nconst SUMMARY_SHARE = 0.08;\n\nexport const SYSTEM_ITEMS: ContextItem[] = [\n  { id: 'system-prompt', label: 'System prompt', tokens: 4_100 },\n  { id: 'system-notes', label: 'Saved notes', tokens: 2_100 },\n];\n\nexport const TOOL_ITEMS: ContextItem[] = [\n  { id: 'tool-search', label: 'web_search results', tokens: 1_900 },\n  { id: 'tool-page', label: 'fetch_page result', tokens: 2_300 },\n  { id: 'tool-sheet', label: 'read_sheet rows', tokens: 2_600 },\n  { id: 'tool-script', label: 'run_script output', tokens: 1_800 },\n];\n\nexport const FILE_ITEMS: ContextItem[] = [\n  { id: 'file-invoices', label: 'q3-invoices.xlsx', tokens: 21_400 },\n  { id: 'file-contract', label: 'brightmoor-contract.pdf', tokens: 18_600 },\n  { id: 'file-pricing', label: 'pricing-page.html', tokens: 9_800 },\n  { id: 'file-notes', label: 'vendor-notes.md', tokens: 3_100 },\n];\n\nconst TURN_TOKENS = [\n  5_200, 7_400, 9_800, 6_100, 12_400, 8_300, 10_900, 7_700, 13_600, 9_200,\n  11_800, 8_600,\n];\n\nfunction turnOf(tokens: number, index: number): ContextItem {\n  const number = index + 1;\n  return { id: `turn-${number}`, label: `Turn ${number}`, tokens };\n}\n\nexport const TURNS: ContextItem[] = TURN_TOKENS.map(turnOf);\n\nexport function sumTokens(items: readonly { tokens: number }[]) {\n  let total = 0;\n  for (const item of items) {\n    total += item.tokens;\n  }\n  return total;\n}\n\n/** What the run holds before any conversation: system, tools and every file. */\nconst SYSTEM_TOKENS = sumTokens(SYSTEM_ITEMS);\n\nconst TOOL_TOKENS = sumTokens(TOOL_ITEMS);\n\nconst FILE_TOKENS = sumTokens(FILE_ITEMS);\n\nexport const BASE_TOKENS = SYSTEM_TOKENS + TOOL_TOKENS + FILE_TOKENS;\n\nexport function usedTokensOf(budget: ContextBudget) {\n  return sumTokens(budget.parts);\n}\n\nexport function freeTokensOf(budget: ContextBudget) {\n  const used = usedTokensOf(budget);\n  const free = budget.limit - budget.reserve - used;\n  return Math.max(0, free);\n}\n\nexport function shareOf(tokens: number, budget: ContextBudget) {\n  return tokens / budget.limit;\n}\n\nexport function percentOf(tokens: number, budget: ContextBudget) {\n  const share = shareOf(tokens, budget);\n  return share * 100;\n}\n\nexport function levelOf(budget: ContextBudget): ContextLevel {\n  const used = usedTokensOf(budget);\n  const share = shareOf(used, budget);\n  if (share < FILLING_FROM) {\n    return 'room';\n  }\n  if (share <= COMPACT_FROM) {\n    return 'filling';\n  }\n  return 'compact';\n}\n\nexport function turnsWithin(conversationTokens: number) {\n  const turns: ContextItem[] = [];\n  let total = 0;\n  for (const turn of TURNS) {\n    const next = total + turn.tokens;\n    if (next > conversationTokens) {\n      break;\n    }\n    turns.push(turn);\n    total = next;\n  }\n  return turns;\n}\n\n/** The tokens of the turns a compaction folds away: every turn of the conversation but the newest ones. */\nexport function foldableTokensOf(budget: ContextBudget) {\n  const conversation = budget.parts.find((part) => part.id === 'conversation');\n  if (!conversation) {\n    return 0;\n  }\n  const foldedCount = conversation.items.length - KEPT_TURNS;\n  if (foldedCount < 1) {\n    return 0;\n  }\n  const folded = conversation.items.slice(0, foldedCount);\n  return sumTokens(folded);\n}\n\nexport function compactionOf(turns: ContextItem[]): Compaction | null {\n  const foldedTurns = turns.length - KEPT_TURNS;\n  if (foldedTurns < 2) {\n    return null;\n  }\n  const folded = turns.slice(0, foldedTurns);\n  const foldedTokens = sumTokens(folded);\n  const rawSummary = foldedTokens * SUMMARY_SHARE;\n  const summaryHundreds = Math.round(rawSummary / 100);\n  const summaryTokens = summaryHundreds * 100;\n  return { foldedTurns, summaryTokens };\n}\n\nfunction partOf(\n  id: ContextPartId,\n  label: string,\n  items: ContextItem[]\n): ContextPart {\n  return { id, label, tokens: sumTokens(items), items };\n}\n\nfunction turnCountOf(count: number) {\n  if (count === 1) {\n    return '1 turn';\n  }\n  return `${count} turns`;\n}\n\ntype BudgetInput = {\n  turns: ContextItem[];\n  removedFileIds: readonly string[];\n  compaction: Compaction | null;\n};\n\nexport function budgetOf({\n  turns,\n  removedFileIds,\n  compaction,\n}: BudgetInput): ContextBudget {\n  const files = FILE_ITEMS.filter((file) => !removedFileIds.includes(file.id));\n  const foldedCount = compaction?.foldedTurns ?? 0;\n  const liveTurns = turns.slice(foldedCount);\n  const parts: ContextPart[] = [\n    partOf('system', 'System', SYSTEM_ITEMS),\n    partOf('tools', 'Tools', TOOL_ITEMS),\n    partOf('files', 'Files', files),\n  ];\n  if (compaction) {\n    const summaryLabel = `Summary of ${turnCountOf(compaction.foldedTurns)}`;\n    const summary = {\n      id: 'summary',\n      label: summaryLabel,\n      tokens: compaction.summaryTokens,\n    };\n    parts.push(partOf('summary', 'Summary', [summary]));\n  }\n  parts.push(partOf('conversation', 'Conversation', liveTurns));\n  return { limit: CONTEXT_LIMIT, reserve: CONTEXT_RESERVE, parts };\n}\n\nexport function compactionLineOf(compaction: Compaction) {\n  const turns = turnCountOf(compaction.foldedTurns);\n  const thousands = compaction.summaryTokens / 1000;\n  const summary = `${thousands.toFixed(1)}k`;\n  return `Folded ${turns} into a ${summary} summary`;\n}\n",
      "type": "registry:component",
      "target": "components/run/_parts/context.ts"
    },
    {
      "path": "registry/ndeko/components/run/_parts/token-count.tsx",
      "content": "'use client';\n\nimport { MotionNumber } from '@/components/core/motion-number';\n\nconst THOUSANDS: Intl.NumberFormatOptions = { maximumFractionDigits: 1 };\n\ntype TokenCountProps = {\n  tokens: number;\n  className?: string;\n};\n\nexport function TokenCount({ tokens, className }: TokenCountProps) {\n  const thousands = tokens / 1000;\n  return (\n    <MotionNumber\n      value={thousands}\n      format={THOUSANDS}\n      suffix='k'\n      className={className}\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "components/run/_parts/token-count.tsx"
    },
    {
      "path": "registry/ndeko/components/run/_parts/use-context-demo.ts",
      "content": "'use client';\n\nimport {\n  useSession,\n  type SessionConfig,\n  type SessionStep,\n} from '@/registry/ndeko/hooks/use-session';\nimport { useTimeouts } from '@/registry/ndeko/hooks/use-timeouts';\n\nimport {\n  budgetOf,\n  compactionOf,\n  TURNS,\n  type Compaction,\n  type ContextSlot,\n} from './context';\n\nexport type CompactStatus = 'idle' | 'compacting';\n\nconst COMPACT_MS = 900;\n\n/** An offer that waits on a person is taken by the demonstration after this long. */\nconst COMPACT_AFTER_MS = 2500;\n\nexport type ContextState = {\n  /** How many turns of the conversation the context holds. */\n  turnCount: number;\n  removedFileIds: string[];\n  compaction: Compaction | null;\n  compactStatus: CompactStatus;\n  /** The part a demonstration points at, in the views that light one part. */\n  spotlight: ContextSlot | null;\n  /** Whether the breakdown is open, in the views that fold it. */\n  isBreakdownOpen: boolean;\n};\n\nexport type ContextAction =\n  | { type: 'turn' }\n  | { type: 'removeFile'; fileId: string }\n  | { type: 'compact' }\n  | { type: 'compacted' }\n  | { type: 'spotlight'; slot: ContextSlot | null }\n  | { type: 'breakdown'; isOpen: boolean };\n\nexport type ContextStep = SessionStep<ContextState, ContextAction>;\n\nexport type ContextSession = SessionConfig<ContextState, ContextAction>;\n\nexport function contextStateAt(turnCount: number): ContextState {\n  return {\n    turnCount,\n    removedFileIds: [],\n    compaction: null,\n    compactStatus: 'idle',\n    spotlight: null,\n    isBreakdownOpen: false,\n  };\n}\n\nfunction turnsIn(state: ContextState) {\n  return TURNS.slice(0, state.turnCount);\n}\n\n/** Compact is offered again only once new turns can be folded into the summary. */\nfunction canCompactIn(state: ContextState) {\n  const turns = turnsIn(state);\n  const next = compactionOf(turns);\n  const foldedNow = state.compaction?.foldedTurns ?? 0;\n  const foldableNext = next?.foldedTurns ?? 0;\n  const hasNewTurnsToFold = foldableNext > foldedNow;\n  const isIdle = state.compactStatus === 'idle';\n  return hasNewTurnsToFold && isIdle;\n}\n\nfunction withTurn(state: ContextState): ContextState {\n  const hasMore = state.turnCount < TURNS.length;\n  if (!hasMore) {\n    return state;\n  }\n  return { ...state, turnCount: state.turnCount + 1 };\n}\n\nfunction withFileRemoved(state: ContextState, fileId: string): ContextState {\n  const removedFileIds = [...state.removedFileIds, fileId];\n  return { ...state, removedFileIds };\n}\n\nfunction withCompactStarted(state: ContextState): ContextState {\n  if (!canCompactIn(state)) {\n    return state;\n  }\n  return { ...state, compactStatus: 'compacting' };\n}\n\n/** The script and the hand of a person may both land the same compaction: only the first one counts. */\nfunction withCompactLanded(state: ContextState): ContextState {\n  if (state.compactStatus !== 'compacting') {\n    return state;\n  }\n  const turns = turnsIn(state);\n  const compaction = compactionOf(turns);\n  return { ...state, compaction, compactStatus: 'idle' };\n}\n\nexport function reduceContext(\n  state: ContextState,\n  action: ContextAction\n): ContextState {\n  switch (action.type) {\n    case 'turn':\n      return withTurn(state);\n    case 'removeFile':\n      return withFileRemoved(state, action.fileId);\n    case 'compact':\n      return withCompactStarted(state);\n    case 'compacted':\n      return withCompactLanded(state);\n    case 'spotlight':\n      return { ...state, spotlight: action.slot };\n    case 'breakdown':\n      return { ...state, isBreakdownOpen: action.isOpen };\n  }\n}\n\nfunction hasCompacted(state: ContextState) {\n  const isCompacting = state.compactStatus === 'compacting';\n  const isCompacted = state.compaction !== null;\n  return isCompacting || isCompacted;\n}\n\n/** A turn that arrives on its own: the conversation going on, which always plays. */\nexport function turnStep(after: number): ContextStep {\n  return { after, action: { type: 'turn' } };\n}\n\n/** A message a person sends: the demonstration presses Send for them until they take over. */\nexport function sendStep(after: number, turnCount: number): ContextStep {\n  return {\n    after,\n    by: 'person',\n    press: 'send',\n    action: { type: 'turn' },\n    isAnswered: (state) => state.turnCount >= turnCount,\n  };\n}\n\n/** Compact pressed by the demonstration once it is offered, then the summary landing. */\nexport const COMPACT_STEPS: ContextStep[] = [\n  {\n    after: COMPACT_AFTER_MS,\n    by: 'person',\n    press: 'compact',\n    action: { type: 'compact' },\n    isAnswered: hasCompacted,\n  },\n  { after: COMPACT_MS, action: { type: 'compacted' } },\n];\n\n/** The demos open on a conversation already under way and fill it one turn at a time. */\nconst OPENING_TURNS = 5;\n\nconst TURN_EVERY_MS = 900;\n\nfunction arrivingTurns(): ContextStep[] {\n  const count = TURNS.length - OPENING_TURNS;\n  return Array.from({ length: count }, () => turnStep(TURN_EVERY_MS));\n}\n\n/** The conversation fills until Compact is offered, the demonstration takes it and the summary lands. */\nexport const FILLING_SCRIPT: ContextStep[] = [\n  ...arrivingTurns(),\n  ...COMPACT_STEPS,\n];\n\n/**\n * Under reduced motion nothing plays: the context rests full, with Compact on offer, which a person\n * can still take.\n */\nexport const FILLING_SESSION: ContextSession = {\n  reducer: reduceContext,\n  initial: contextStateAt(OPENING_TURNS),\n  resting: contextStateAt(TURNS.length),\n  script: FILLING_SCRIPT,\n  loopAfter: 3400,\n};\n\n/** A context that fills turn by turn, played as a session: turns arrive, a person removes files and compacts. */\nexport function useContextSession(session: ContextSession) {\n  const {\n    ref,\n    isInView,\n    isAutoplaying,\n    state,\n    act,\n    pressed,\n    isFinished,\n    restart,\n  } = useSession(session);\n  const { schedule, clearAll } = useTimeouts();\n  const turns = turnsIn(state);\n  const { removedFileIds, compaction } = state;\n  const budget = budgetOf({ turns, removedFileIds, compaction });\n  const budgetBefore = budgetOf({ turns, removedFileIds, compaction: null });\n  const canCompact = canCompactIn(state);\n  const canReplay = isFinished && !isAutoplaying;\n\n  function removeFile(fileId: string) {\n    act({ type: 'removeFile', fileId });\n  }\n\n  function compact() {\n    if (!canCompact) {\n      return;\n    }\n    act({ type: 'compact' });\n    schedule(() => act({ type: 'compacted' }), COMPACT_MS);\n  }\n\n  function send() {\n    act({ type: 'turn' });\n  }\n\n  function setBreakdownOpen(isOpen: boolean) {\n    act({ type: 'breakdown', isOpen });\n  }\n\n  function replay() {\n    clearAll();\n    restart();\n  }\n\n  return {\n    ref,\n    isInView,\n    isAutoplaying,\n    canReplay,\n    pressed,\n    turns,\n    budget,\n    budgetBefore,\n    compaction,\n    compactStatus: state.compactStatus,\n    canCompact,\n    spotlight: state.spotlight,\n    isBreakdownOpen: state.isBreakdownOpen,\n    removeFile,\n    compact,\n    send,\n    setBreakdownOpen,\n    replay,\n  };\n}\n",
      "type": "registry:component",
      "target": "components/run/_parts/use-context-demo.ts"
    }
  ],
  "categories": [
    "components",
    "run"
  ],
  "type": "registry:component"
}