{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "run-07",
  "title": "Context ring",
  "description": "A ring for a composer corner that closes toward the limit as messages are sent, turns to warning above 85% and opens the breakdown.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://ui.ndekocode.dev/r/badge.json",
    "https://ui.ndekocode.dev/r/button.json",
    "https://ui.ndekocode.dev/r/chat-motion.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/popover.json",
    "https://ui.ndekocode.dev/r/theme.json",
    "https://ui.ndekocode.dev/r/tooltip.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-07.tsx",
      "content": "'use client';\n\nimport { useId } from 'react';\n\nimport { WordRoll } from '@/components/core/word-roll';\nimport { useKeptHeight } from '@/registry/ndeko/hooks/use-kept-height';\nimport {\n  pressedQuietOf,\n  pressedSolidOf,\n  spokenUnless,\n} from '@/registry/ndeko/lib/demonstration';\nimport {\n  ArrowUpIcon,\n  CheckIcon,\n  RotateCcwIcon,\n} from '@/registry/ndeko/lib/icons';\nimport { cn } from '@/registry/ndeko/lib/utils';\nimport { Badge } from '@/registry/ndeko/ui/badge';\nimport { Button } from '@/registry/ndeko/ui/button';\nimport { Swap } from '@/registry/ndeko/ui/chat-motion';\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTitle,\n  PopoverTrigger,\n} from '@/registry/ndeko/ui/popover';\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from '@/registry/ndeko/ui/tooltip';\n\nimport { Collapse } from './_parts/collapse';\nimport {\n  compactionLineOf,\n  freeTokensOf,\n  LEVEL_LABELS,\n  LEVEL_TONE,\n  LEVEL_WORD,\n  levelOf,\n  PART_FILL,\n  RESERVE_STYLE,\n  shareOf,\n  TURNS,\n  usedTokensOf,\n  type Compaction,\n  type ContextBudget,\n} from './_parts/context';\nimport { ClockGate } from '@/registry/ndeko/ui/live-duration';\nimport { LiveRegion } from '@/registry/ndeko/ui/live-region';\nimport { MorphButton, type MorphState } from '@/registry/ndeko/ui/morph-button';\nimport { TokenCount } from './_parts/token-count';\nimport {\n  COMPACT_STEPS,\n  contextStateAt,\n  reduceContext,\n  sendStep,\n  useContextSession,\n  type CompactStatus,\n  type ContextSession,\n  type ContextStep,\n} from './_parts/use-context-demo';\n\nconst RING_SIZE = 28;\n\nconst RING_STROKE = 3;\n\nconst RING_RADIUS = (RING_SIZE - RING_STROKE) / 2;\n\nconst RING_CENTER = RING_SIZE / 2;\n\nconst CIRCUMFERENCE = 2 * Math.PI * RING_RADIUS;\n\nconst WARNING_FROM = 0.85;\n\nconst COMPACT_LABELS: Record<MorphState, string> = {\n  idle: 'Compact older turns',\n  busy: 'Compacting',\n  done: 'Compacted',\n};\n\nconst RING_TRIGGER =\n  'focus-visible:ring-ring focus-visible:ring-offset-background inline-flex size-8 cursor-pointer items-center justify-center rounded-full transition-[background-color,scale] duration-200 outline-none focus-visible:ring-2 focus-visible:ring-offset-2';\n\nfunction percentLabelOf(share: number) {\n  const percent = Math.round(share * 100);\n  return `${percent}%`;\n}\n\nfunction dashOffsetOf(share: number) {\n  const bounded = Math.min(1, Math.max(0, share));\n  const remaining = 1 - bounded;\n  return CIRCUMFERENCE * remaining;\n}\n\nfunction ringToneOf(isWarning: boolean) {\n  if (isWarning) {\n    return {\n      trigger: 'bg-warning/10 hover:bg-warning/15',\n      track: 'stroke-warning/25',\n      fill: 'stroke-warning',\n    };\n  }\n  return {\n    trigger: 'hover:bg-foreground/5',\n    track: 'stroke-foreground/10',\n    fill: 'stroke-chart-1',\n  };\n}\n\nfunction morphStateOf(status: CompactStatus): MorphState {\n  if (status === 'compacting') {\n    return 'busy';\n  }\n  return 'idle';\n}\n\nfunction traceOf(compaction: Compaction | null, isCompactShown: boolean) {\n  if (isCompactShown) {\n    return null;\n  }\n  return compaction;\n}\n\nfunction RingGlyph({\n  share,\n  isWarning,\n}: {\n  share: number;\n  isWarning: boolean;\n}) {\n  const tone = ringToneOf(isWarning);\n  const dashOffset = dashOffsetOf(share);\n\n  return (\n    <svg\n      aria-hidden='true'\n      width={RING_SIZE}\n      height={RING_SIZE}\n      viewBox={`0 0 ${RING_SIZE} ${RING_SIZE}`}\n      className='-rotate-90'\n    >\n      <circle\n        cx={RING_CENTER}\n        cy={RING_CENTER}\n        r={RING_RADIUS}\n        fill='none'\n        strokeWidth={RING_STROKE}\n        className={cn('transition-[stroke] duration-300', tone.track)}\n      />\n      <circle\n        cx={RING_CENTER}\n        cy={RING_CENTER}\n        r={RING_RADIUS}\n        fill='none'\n        strokeWidth={RING_STROKE}\n        strokeLinecap='round'\n        strokeDasharray={CIRCUMFERENCE}\n        strokeDashoffset={dashOffset}\n        className={cn(\n          'transition-[stroke-dashoffset,stroke] duration-500 ease-(--ease-out-strong)',\n          tone.fill\n        )}\n      />\n    </svg>\n  );\n}\n\nfunction BreakdownRows({ budget }: { budget: ContextBudget }) {\n  const free = freeTokensOf(budget);\n  const parts = budget.parts.filter((part) => part.tokens > 0);\n\n  return (\n    <ul aria-label='What fills the context' className='flex flex-col'>\n      {parts.map((part) => (\n        <li key={part.id} className='flex h-7 items-center gap-2.5 text-[13px]'>\n          <span\n            aria-hidden='true'\n            className='size-2 shrink-0 rounded-full'\n            style={{ backgroundColor: PART_FILL[part.id] }}\n          />\n          <span className='min-w-0 flex-1 truncate'>{part.label}</span>\n          <TokenCount tokens={part.tokens} className='text-muted-foreground' />\n        </li>\n      ))}\n      <li className='flex h-7 items-center gap-2.5 text-[13px]'>\n        <span\n          aria-hidden='true'\n          className='bg-foreground/[0.08] size-2 shrink-0 rounded-full'\n        />\n        <span className='min-w-0 flex-1 truncate'>Free</span>\n        <TokenCount tokens={free} className='text-muted-foreground' />\n      </li>\n      <li className='flex h-7 items-center gap-2.5 text-[13px]'>\n        <span\n          aria-hidden='true'\n          className='size-2 shrink-0 rounded-full'\n          style={RESERVE_STYLE}\n        />\n        <span className='min-w-0 flex-1 truncate'>Held for the answer</span>\n        <TokenCount tokens={budget.reserve} className='text-muted-foreground' />\n      </li>\n    </ul>\n  );\n}\n\nconst PLAIN_TITLE = <p className='text-sm leading-none font-medium'>Context</p>;\n\nexport type ContextBreakdownProps = {\n  budget: ContextBudget;\n  canCompact?: boolean;\n  compactStatus?: CompactStatus;\n  compaction?: Compaction | null;\n  onCompact?: () => void;\n  /** The heading of the panel. A popover passes its own title element so it is named by it. */\n  title?: React.ReactNode;\n  /** Shows Compact held down, for a demonstration that presses it. */\n  isCompactPressed?: boolean;\n  className?: string;\n};\n\n/**\n * What fills the context, part by part, with Compact when older turns can be folded. The ring opens\n * it in a popover, and a page can draw it in place.\n */\nexport function ContextBreakdown({\n  budget,\n  canCompact = false,\n  compactStatus = 'idle',\n  compaction = null,\n  onCompact,\n  title = PLAIN_TITLE,\n  isCompactPressed = false,\n  className,\n}: ContextBreakdownProps) {\n  const used = usedTokensOf(budget);\n  const level = levelOf(budget);\n  const isCompacting = compactStatus === 'compacting';\n  const isCompactShown = canCompact || isCompacting;\n  const trace = traceOf(compaction, isCompactShown);\n\n  return (\n    <div className={cn('flex flex-col gap-3', className)}>\n      <div className='flex items-start justify-between gap-3'>\n        <div className='min-w-0'>\n          {title}\n          <p\n            className={cn(\n              'mt-1 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      </div>\n      <BreakdownRows budget={budget} />\n      {isCompactShown && (\n        <MorphButton\n          type='button'\n          size='sm'\n          variant='outline'\n          state={morphStateOf(compactStatus)}\n          labels={COMPACT_LABELS}\n          onClick={onCompact}\n          className={cn('self-start', pressedQuietOf(isCompactPressed))}\n        />\n      )}\n      {trace && (\n        <p className='text-muted-foreground flex items-center gap-2 text-xs'>\n          <CheckIcon className='text-success-foreground size-3.5' />\n          {compactionLineOf(trace)}\n        </p>\n      )}\n    </div>\n  );\n}\n\n/** `inline` makes the ring a toggle and leaves the breakdown to the page, which draws `ContextBreakdown` where it wants it. */\nexport type ContextRingSurface = 'popover' | 'inline';\n\nexport type ContextRingProps = {\n  budget: ContextBudget;\n  canCompact?: boolean;\n  compactStatus?: CompactStatus;\n  compaction?: Compaction | null;\n  onCompact?: () => void;\n  as?: ContextRingSurface;\n  /** With `inline`: whether the page shows the breakdown. */\n  isOpen?: boolean;\n  onOpenChange?: (isOpen: boolean) => void;\n  /** With `inline`: the id of the element the page draws the breakdown in. */\n  breakdownId?: string;\n  /** Shows the ring held down, for a demonstration that presses it. */\n  isPressed?: boolean;\n  className?: string;\n};\n\nexport function ContextRing({\n  budget,\n  canCompact = false,\n  compactStatus = 'idle',\n  compaction = null,\n  onCompact,\n  as = 'popover',\n  isOpen = false,\n  onOpenChange,\n  breakdownId,\n  isPressed = false,\n  className,\n}: ContextRingProps) {\n  const used = usedTokensOf(budget);\n  const share = shareOf(used, budget);\n  const isWarning = share > WARNING_FROM;\n  const percentLabel = percentLabelOf(share);\n  const tone = ringToneOf(isWarning);\n  const triggerLabel = `Context ${percentLabel} used, open the breakdown`;\n\n  function toggle() {\n    onOpenChange?.(!isOpen);\n  }\n\n  if (as === 'inline') {\n    return (\n      <Tooltip>\n        <TooltipTrigger\n          render={\n            <button\n              type='button'\n              aria-label={triggerLabel}\n              aria-expanded={isOpen}\n              aria-controls={breakdownId}\n              onClick={toggle}\n              className={cn(\n                RING_TRIGGER,\n                'active:scale-[0.97]',\n                tone.trigger,\n                pressedQuietOf(isPressed),\n                className\n              )}\n            />\n          }\n        >\n          <RingGlyph share={share} isWarning={isWarning} />\n        </TooltipTrigger>\n        <TooltipContent>{percentLabel} of the context used</TooltipContent>\n      </Tooltip>\n    );\n  }\n\n  return (\n    <Popover>\n      <Tooltip>\n        <TooltipTrigger\n          render={\n            <PopoverTrigger\n              render={\n                <button\n                  type='button'\n                  aria-label={triggerLabel}\n                  className={cn(RING_TRIGGER, tone.trigger, className)}\n                />\n              }\n            />\n          }\n        >\n          <RingGlyph share={share} isWarning={isWarning} />\n        </TooltipTrigger>\n        <TooltipContent>{percentLabel} of the context used</TooltipContent>\n      </Tooltip>\n      <PopoverContent side='top' align='end' className='w-72 p-3'>\n        <ContextBreakdown\n          budget={budget}\n          canCompact={canCompact}\n          compactStatus={compactStatus}\n          compaction={compaction}\n          onCompact={onCompact}\n          title={<PopoverTitle>Context</PopoverTitle>}\n        />\n      </PopoverContent>\n    </Popover>\n  );\n}\n\nconst MESSAGES = [\n  'Start the quarterly review for Brightmoor Supply',\n  'Use the Q3 invoices I attached',\n  'Check their pricing page as well',\n  'Which lines are above the list price?',\n  'Add the contract terms on bulk orders',\n  'How much did we overpay in total?',\n  'Split that by month',\n  'Draft an email asking for a credit',\n  'Make it shorter and less formal',\n  'Attach the comparison as a CSV',\n  'Add a line about the October order',\n  'Send it to accounts once I approve',\n];\n\n/** The demo opens on a conversation already half way to the limit. */\nconst OPENING_TURNS = 6;\n\nconst SEND_EVERY_MS = 1100;\n\nfunction sendSteps(): ContextStep[] {\n  const steps: ContextStep[] = [];\n  for (let count = OPENING_TURNS + 1; count <= TURNS.length; count += 1) {\n    steps.push(sendStep(SEND_EVERY_MS, count));\n  }\n  return steps;\n}\n\nfunction ringStep(after: number, isOpen: boolean): ContextStep {\n  return {\n    after,\n    by: 'demo',\n    press: 'ring',\n    action: { type: 'breakdown', isOpen },\n  };\n}\n\n/** Messages go out until the ring warns, the breakdown opens, the older turns are folded and it closes. */\nconst SCRIPT: ContextStep[] = [\n  ...sendSteps(),\n  ringStep(900, true),\n  ...COMPACT_STEPS,\n  ringStep(2400, false),\n];\n\nconst RESTING = { ...contextStateAt(TURNS.length), isBreakdownOpen: true };\n\nconst SESSION: ContextSession = {\n  reducer: reduceContext,\n  initial: contextStateAt(OPENING_TURNS),\n  resting: RESTING,\n  script: SCRIPT,\n  loopAfter: 2600,\n};\n\nfunction nextMessageOf(sentCount: number) {\n  return MESSAGES[sentCount] ?? null;\n}\n\nfunction ringAnnouncementOf(budget: ContextBudget, sentCount: number) {\n  if (sentCount === OPENING_TURNS) {\n    return '';\n  }\n  const used = usedTokensOf(budget);\n  const share = shareOf(used, budget);\n  const percent = percentLabelOf(share);\n  const level = levelOf(budget);\n  return `Message sent. Context ${percent} used, ${LEVEL_WORD[level]}`;\n}\n\nexport default function Run07() {\n  const {\n    ref,\n    isInView,\n    isAutoplaying,\n    pressed,\n    turns,\n    budget,\n    compaction,\n    compactStatus,\n    canCompact,\n    isBreakdownOpen,\n    compact,\n    send,\n    setBreakdownOpen,\n    replay,\n  } = useContextSession(SESSION);\n  const breakdownId = useId();\n  const sentCount = turns.length;\n  const nextMessage = nextMessageOf(sentCount);\n  const hasNext = nextMessage !== null;\n  const announcement = ringAnnouncementOf(budget, sentCount);\n\n  useKeptHeight(ref);\n\n  return (\n    <div\n      ref={ref}\n      className='flex min-h-[27.5rem] w-full max-w-[26rem] flex-col gap-2'\n    >\n      <ClockGate isActive={isInView}>\n        <div className='bg-card hairline-ring ring-border flex flex-col rounded-xl p-2.5 shadow-[0_1px_2px_rgb(0_0_0/0.04)]'>\n          <p className='min-h-10 px-1.5 pt-1 text-[13px] leading-5'>\n            <Swap swapKey={nextMessage ?? 'done'}>\n              {hasNext && nextMessage}\n              {!hasNext && (\n                <span className='text-muted-foreground'>\n                  That was the last demo message\n                </span>\n              )}\n            </Swap>\n          </p>\n          <div className='mt-2 flex items-center gap-2'>\n            <Badge variant='secondary'>Claude Haiku</Badge>\n            <span className='flex-1' />\n            <ContextRing\n              as='inline'\n              budget={budget}\n              isOpen={isBreakdownOpen}\n              onOpenChange={setBreakdownOpen}\n              breakdownId={breakdownId}\n              isPressed={pressed === 'ring'}\n            />\n            <Button\n              type='button'\n              size='icon'\n              aria-label='Send this demo message'\n              disabled={!hasNext}\n              onClick={send}\n              className={pressedSolidOf(pressed === 'send')}\n            >\n              <ArrowUpIcon />\n            </Button>\n          </div>\n          <Collapse open={isBreakdownOpen} id={breakdownId}>\n            <ContextBreakdown\n              budget={budget}\n              canCompact={canCompact}\n              compactStatus={compactStatus}\n              compaction={compaction}\n              onCompact={compact}\n              isCompactPressed={pressed === 'compact'}\n              className='bg-foreground/[0.03] mt-2.5 rounded-lg p-3'\n            />\n          </Collapse>\n        </div>\n      </ClockGate>\n      <div className='flex h-7 items-center justify-between gap-2'>\n        <p className='text-muted-foreground px-1 text-xs'>\n          Each message adds its turn to the context\n        </p>\n        <Button\n          type='button'\n          variant='ghost'\n          size='sm'\n          onClick={replay}\n          className='text-muted-foreground'\n        >\n          <RotateCcwIcon />\n          Start over\n        </Button>\n      </div>\n      <LiveRegion message={spokenUnless(isAutoplaying, announcement)} />\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/run/run-07.tsx"
    },
    {
      "path": "registry/ndeko/components/run/_parts/collapse.tsx",
      "content": "'use client';\n\nimport { motion, type Transition } from 'motion/react';\n\nimport { usePrefersReducedMotion } from '@/registry/ndeko/hooks/use-prefers-reduced-motion';\nimport { EASE_OUT_STRONG, INSTANT } from '@/registry/ndeko/lib/motion-presets';\n\nconst OPEN = { height: 'auto', opacity: 1 };\n\nconst CLOSED = { height: 0, opacity: 0 };\n\nconst OPENING: Transition = { duration: 0.2, ease: EASE_OUT_STRONG };\n\nconst CLOSING: Transition = { duration: 0.14, ease: EASE_OUT_STRONG };\n\nfunction targetOf(open: boolean) {\n  if (open) {\n    return OPEN;\n  }\n  return CLOSED;\n}\n\nfunction transitionOf(open: boolean, shouldReduceMotion: boolean | null) {\n  if (shouldReduceMotion) {\n    return INSTANT;\n  }\n  if (open) {\n    return OPENING;\n  }\n  return CLOSING;\n}\n\ntype CollapseProps = {\n  open: boolean;\n  id?: string;\n  children: React.ReactNode;\n};\n\n/**\n * A panel that folds and keeps its content in the page. Open, it takes the height of its content\n * without animating it, so a row that arrives inside moves once, by its own presence. The padding\n * leaves room for focus rings and the margin takes it back, also when the panel is closed.\n */\nexport function Collapse({ open, id, children }: CollapseProps) {\n  const shouldReduceMotion = usePrefersReducedMotion();\n\n  return (\n    <motion.div\n      id={id}\n      inert={!open}\n      initial={false}\n      animate={targetOf(open)}\n      transition={transitionOf(open, shouldReduceMotion)}\n      className='-m-1 overflow-hidden p-1'\n    >\n      {children}\n    </motion.div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/run/_parts/collapse.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"
}