{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "agent-11",
  "title": "Task checklist",
  "description": "The to-do list of an agent under a header that counts 2 of 5 and carries the ring of the whole: the running step shows a ring that fills with its percentage rolling, and a done step is struck through by a line that draws from left to right.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://ui.ndekocode.dev/r/agent-run.json",
    "https://ui.ndekocode.dev/r/chat-motion.json",
    "https://ui.ndekocode.dev/r/demonstration.json",
    "https://ui.ndekocode.dev/r/drawn-check.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/mesh-tile.json",
    "https://ui.ndekocode.dev/r/motion-number.json",
    "https://ui.ndekocode.dev/r/motion-presets.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/utils.json"
  ],
  "files": [
    {
      "path": "registry/ndeko/components/agent/agent-11.tsx",
      "content": "'use client';\n\nimport { AnimatePresence, motion } from 'motion/react';\nimport { useId } from 'react';\n\nimport { MotionNumber } from '@/components/core/motion-number';\nimport { useKeptHeight } from '@/registry/ndeko/hooks/use-kept-height';\nimport { usePrefersReducedMotion } from '@/registry/ndeko/hooks/use-prefers-reduced-motion';\nimport {\n  useSession,\n  type SessionStep,\n} from '@/registry/ndeko/hooks/use-session';\nimport type { RunAgent } from '@/registry/ndeko/lib/agent-run';\nimport {\n  pressedQuietOf,\n  spokenUnless,\n} from '@/registry/ndeko/lib/demonstration';\nimport { ChevronDownIcon, CircleIcon } from '@/registry/ndeko/lib/icons';\nimport { rowPresenceOf } from '@/registry/ndeko/lib/motion-presets';\nimport { cn } from '@/registry/ndeko/lib/utils';\nimport { Fold, Swap } from '@/registry/ndeko/ui/chat-motion';\nimport { DrawnCheck } from '@/registry/ndeko/ui/drawn-check';\nimport { ClockGate } from '@/registry/ndeko/ui/live-duration';\nimport { LiveRegion } from '@/registry/ndeko/ui/live-region';\nimport { ReplayButton } from '@/registry/ndeko/ui/replay-button';\n\nimport { AgentTile } from './_parts/agent-tile';\nimport { RESEARCHER } from './_parts/crew';\nimport { useOpenState } from './_parts/use-open-state';\n\nexport type ChecklistStepStatus = 'pending' | 'running' | 'done';\n\nexport type ChecklistStep = {\n  id: string;\n  title: string;\n  status: ChecklistStepStatus;\n  /** How far the running step is, from 0 to 100. */\n  progress?: number;\n  detail?: string;\n};\n\nconst STATUS_WORD: Record<ChecklistStepStatus, string> = {\n  pending: 'Not started',\n  running: 'Running',\n  done: 'Done',\n};\n\nconst HEADER_RING_SIZE = 28;\n\nconst HEADER_RING_STROKE = 3;\n\nconst STEP_RING_SIZE = 18;\n\nconst STEP_RING_STROKE = 2.5;\n\nfunction circumferenceOf(size: number, stroke: number) {\n  const radius = (size - stroke) / 2;\n  return 2 * Math.PI * radius;\n}\n\nfunction isDone(step: ChecklistStep) {\n  return step.status === 'done';\n}\n\nfunction progressOf(step: ChecklistStep) {\n  return step.progress ?? 0;\n}\n\nfunction wholeShareOf(steps: ChecklistStep[]) {\n  if (steps.length === 0) {\n    return 0;\n  }\n  let total = 0;\n  for (const step of steps) {\n    if (isDone(step)) {\n      total += 100;\n    }\n    if (step.status === 'running') {\n      total += progressOf(step);\n    }\n  }\n  return total / (steps.length * 100);\n}\n\nfunction titleToneOf(status: ChecklistStepStatus) {\n  if (status === 'done') {\n    return 'text-muted-foreground';\n  }\n  if (status === 'running') {\n    return 'text-foreground';\n  }\n  return 'text-foreground/70';\n}\n\nfunction strikeOf(status: ChecklistStepStatus) {\n  if (status === 'done') {\n    return 'scale-x-100';\n  }\n  return 'scale-x-0';\n}\n\nfunction ringFillOf(isComplete: boolean) {\n  if (isComplete) {\n    return 'stroke-success';\n  }\n  return 'stroke-primary';\n}\n\nfunction chevronTurnOf(isOpen: boolean) {\n  if (isOpen) {\n    return 'rotate-180';\n  }\n  return 'rotate-0';\n}\n\ntype RingProps = {\n  size: number;\n  stroke: number;\n  share: number;\n  fillClassName: string;\n};\n\nfunction Ring({ size, stroke, share, fillClassName }: RingProps) {\n  const radius = (size - stroke) / 2;\n  const center = size / 2;\n  const circumference = circumferenceOf(size, stroke);\n  const bounded = Math.min(1, Math.max(0, share));\n  const dashOffset = circumference * (1 - bounded);\n\n  return (\n    <svg\n      aria-hidden='true'\n      width={size}\n      height={size}\n      viewBox={`0 0 ${size} ${size}`}\n      className='-rotate-90'\n    >\n      <circle\n        cx={center}\n        cy={center}\n        r={radius}\n        fill='none'\n        strokeWidth={stroke}\n        className='stroke-foreground/10'\n      />\n      <circle\n        cx={center}\n        cy={center}\n        r={radius}\n        fill='none'\n        strokeWidth={stroke}\n        strokeLinecap='round'\n        strokeDasharray={circumference}\n        strokeDashoffset={dashOffset}\n        className={cn(\n          'transition-[stroke-dashoffset,stroke] duration-500 ease-(--ease-out-strong) motion-reduce:transition-none',\n          fillClassName\n        )}\n      />\n    </svg>\n  );\n}\n\nfunction StepGlyph({ step }: { step: ChecklistStep }) {\n  if (step.status === 'pending') {\n    return <CircleIcon className='text-muted-foreground/60 size-4' />;\n  }\n  if (step.status === 'running') {\n    return (\n      <Ring\n        size={STEP_RING_SIZE}\n        stroke={STEP_RING_STROKE}\n        share={progressOf(step) / 100}\n        fillClassName='stroke-primary'\n      />\n    );\n  }\n  return <DrawnCheck tone='success' entrance='draw' />;\n}\n\nfunction StepMark({ step }: { step: ChecklistStep }) {\n  return (\n    <span className='inline-flex size-5 shrink-0 items-center justify-center'>\n      <Swap swapKey={step.status} className='size-5 justify-center'>\n        <StepGlyph step={step} />\n      </Swap>\n      <span className='sr-only'>{STATUS_WORD[step.status]}</span>\n    </span>\n  );\n}\n\nfunction StepRow({ step }: { step: ChecklistStep }) {\n  const shouldReduceMotion = usePrefersReducedMotion();\n  const rowPresence = rowPresenceOf(shouldReduceMotion);\n  const isRunning = step.status === 'running';\n  const hasDetail = step.detail !== undefined;\n  const showsDetail = isRunning && hasDetail;\n  const percent = Math.round(progressOf(step));\n\n  return (\n    <li data-status={step.status} className='flex items-start gap-3 py-1.5'>\n      <StepMark step={step} />\n      <span className='min-w-0 flex-1'>\n        <span className='relative inline-block max-w-full align-top'>\n          <span\n            className={cn(\n              'block truncate text-[13px] leading-5 transition-colors duration-300 ease-(--ease-out-strong)',\n              titleToneOf(step.status)\n            )}\n          >\n            {step.title}\n          </span>\n          <span\n            aria-hidden='true'\n            className={cn(\n              'bg-muted-foreground/70 absolute inset-x-0 top-2.5 h-px origin-left transition-[scale] duration-300 ease-(--ease-out-strong) motion-reduce:transition-none',\n              strikeOf(step.status)\n            )}\n          />\n        </span>\n        <AnimatePresence initial={false}>\n          {showsDetail && (\n            <motion.span\n              key='detail'\n              className='text-muted-foreground block overflow-hidden text-xs leading-5'\n              {...rowPresence}\n            >\n              {step.detail}\n            </motion.span>\n          )}\n        </AnimatePresence>\n      </span>\n      <AnimatePresence initial={false}>\n        {isRunning && (\n          <motion.span\n            key='percent'\n            className='text-muted-foreground inline-flex shrink-0 text-xs leading-5'\n            {...rowPresence}\n          >\n            <MotionNumber value={percent} suffix='%' />\n          </motion.span>\n        )}\n      </AnimatePresence>\n    </li>\n  );\n}\n\nexport type TaskChecklistProps = {\n  title: string;\n  agent: Pick<RunAgent, 'name' | 'series'>;\n  steps: ChecklistStep[];\n  isOpen?: boolean;\n  onOpenChange?: (isOpen: boolean) => void;\n  /** Shows the header held down, for a demonstration that presses it. */\n  isTogglePressed?: boolean;\n  className?: string;\n};\n\nexport function TaskChecklist({\n  title,\n  agent,\n  steps,\n  isOpen,\n  onOpenChange,\n  isTogglePressed = false,\n  className,\n}: TaskChecklistProps) {\n  const listId = useId();\n  const openState = useOpenState({ isOpen, defaultOpen: true, onOpenChange });\n  const doneCount = steps.filter(isDone).length;\n  const share = wholeShareOf(steps);\n  const isComplete = steps.length > 0 && doneCount === steps.length;\n  const ringFill = ringFillOf(isComplete);\n\n  return (\n    <section\n      aria-label={`${agent.name}'s checklist: ${title}`}\n      className={cn(\n        'bg-card hairline-ring ring-border rounded-xl p-2 shadow-[0_1px_2px_rgb(0_0_0/0.04)]',\n        className\n      )}\n    >\n      <button\n        type='button'\n        aria-expanded={openState.isOpen}\n        aria-controls={listId}\n        onClick={openState.toggle}\n        className={cn(\n          'hover:bg-foreground/[0.03] focus-visible:ring-ring/25 flex min-h-12 w-full cursor-pointer items-center gap-3 rounded-lg px-2 py-2 text-start transition-[background-color,scale] outline-none focus-visible:ring-[3px]',\n          pressedQuietOf(isTogglePressed)\n        )}\n      >\n        <AgentTile name={agent.name} series={agent.series} size='lg' />\n        <span className='flex min-w-0 flex-1 flex-col'>\n          <span className='truncate text-sm font-medium'>{title}</span>\n          <span className='text-muted-foreground text-xs'>\n            <MotionNumber value={doneCount} /> of{' '}\n            <MotionNumber value={steps.length} /> done\n          </span>\n        </span>\n        <Ring\n          size={HEADER_RING_SIZE}\n          stroke={HEADER_RING_STROKE}\n          share={share}\n          fillClassName={ringFill}\n        />\n        <ChevronDownIcon\n          className={cn(\n            'text-muted-foreground size-4 shrink-0 transition-transform duration-200 ease-(--ease-out-strong)',\n            chevronTurnOf(openState.isOpen)\n          )}\n        />\n      </button>\n      <Fold open={openState.isOpen} id={listId}>\n        <ol aria-label='Steps' className='flex flex-col px-2 pt-1 pb-2'>\n          {steps.map((step) => (\n            <StepRow key={step.id} step={step} />\n          ))}\n        </ol>\n      </Fold>\n    </section>\n  );\n}\n\ntype StepSpec = {\n  id: string;\n  title: string;\n  detail: string;\n};\n\nconst SPECS: StepSpec[] = [\n  {\n    id: 'collect',\n    title: 'Collect the Q3 invoices',\n    detail: 'Reading 117 invoice lines',\n  },\n  {\n    id: 'match',\n    title: 'Match each line to the price list',\n    detail: 'Comparing 42 products',\n  },\n  {\n    id: 'flag',\n    title: 'Flag the lines above the list price',\n    detail: 'Checking 9 suspect lines',\n  },\n  {\n    id: 'draft',\n    title: 'Draft the credit request',\n    detail: 'Writing to Brightmoor accounts',\n  },\n  {\n    id: 'send',\n    title: 'Send the summary to finance',\n    detail: 'Preparing one email',\n  },\n];\n\nconst TITLE = 'Prepare the Q3 vendor review';\n\ntype ChecklistState = {\n  steps: ChecklistStep[];\n  isOpen: boolean;\n};\n\ntype ChecklistAction =\n  | { type: 'start'; id: string }\n  | { type: 'progress'; id: string; percent: number }\n  | { type: 'finish'; id: string }\n  | { type: 'open'; isOpen: boolean }\n  | { type: 'hold' };\n\ntype ChecklistScriptStep = SessionStep<ChecklistState, ChecklistAction>;\n\nfunction pendingStep(spec: StepSpec): ChecklistStep {\n  return { id: spec.id, title: spec.title, status: 'pending' };\n}\n\nconst INITIAL: ChecklistState = {\n  steps: SPECS.map(pendingStep),\n  isOpen: true,\n};\n\nfunction withStep(\n  state: ChecklistState,\n  id: string,\n  change: (step: ChecklistStep) => ChecklistStep\n): ChecklistState {\n  const steps = state.steps.map((step) => {\n    if (step.id === id) {\n      return change(step);\n    }\n    return step;\n  });\n  return { ...state, steps };\n}\n\nfunction detailOf(id: string) {\n  const spec = SPECS.find((candidate) => candidate.id === id);\n  return spec?.detail;\n}\n\nfunction reducer(\n  state: ChecklistState,\n  action: ChecklistAction\n): ChecklistState {\n  switch (action.type) {\n    case 'start':\n      return withStep(state, action.id, (step) => ({\n        ...step,\n        status: 'running',\n        progress: 0,\n        detail: detailOf(step.id),\n      }));\n    case 'progress':\n      return withStep(state, action.id, (step) => ({\n        ...step,\n        progress: action.percent,\n      }));\n    case 'finish':\n      return withStep(state, action.id, (step) => ({\n        ...step,\n        status: 'done',\n        progress: 100,\n        detail: undefined,\n      }));\n    case 'open':\n      return { ...state, isOpen: action.isOpen };\n    case 'hold':\n      return state;\n  }\n}\n\nconst PROGRESS_STOPS = [22, 58, 86];\n\nfunction scriptOfStep(id: string): ChecklistScriptStep[] {\n  const steps: ChecklistScriptStep[] = [\n    { after: 500, action: { type: 'start', id } },\n  ];\n  for (const percent of PROGRESS_STOPS) {\n    steps.push({ after: 500, action: { type: 'progress', id, percent } });\n  }\n  steps.push({ after: 450, action: { type: 'finish', id } });\n  return steps;\n}\n\nfunction scriptOfAllSteps() {\n  const steps: ChecklistScriptStep[] = [];\n  for (const spec of SPECS) {\n    steps.push(...scriptOfStep(spec.id));\n  }\n  return steps;\n}\n\nconst SCRIPT: ChecklistScriptStep[] = [\n  ...scriptOfAllSteps(),\n  {\n    after: 1400,\n    by: 'demo',\n    press: 'toggle',\n    action: { type: 'open', isOpen: false },\n  },\n  { after: 2600, action: { type: 'hold' } },\n];\n\nfunction restingStepOf(spec: StepSpec, index: number): ChecklistStep {\n  if (index < 3) {\n    return { ...pendingStep(spec), status: 'done', progress: 100 };\n  }\n  if (index === 3) {\n    return {\n      ...pendingStep(spec),\n      status: 'running',\n      progress: 58,\n      detail: spec.detail,\n    };\n  }\n  return pendingStep(spec);\n}\n\nconst RESTING: ChecklistState = {\n  steps: SPECS.map(restingStepOf),\n  isOpen: true,\n};\n\nconst SESSION = {\n  reducer,\n  initial: INITIAL,\n  resting: RESTING,\n  script: SCRIPT,\n  loopAfter: 3000,\n};\n\nfunction announcementOf(steps: ChecklistStep[]) {\n  const running = steps.find((step) => step.status === 'running');\n  const doneCount = steps.filter(isDone).length;\n  if (running) {\n    const position = steps.indexOf(running) + 1;\n    return `Step ${position} of ${steps.length}: ${running.title}`;\n  }\n  if (doneCount === steps.length) {\n    return 'The checklist is done';\n  }\n  return '';\n}\n\nexport default function Agent11() {\n  const {\n    ref,\n    isInView,\n    isAutoplaying,\n    state,\n    act,\n    pressed,\n    isFinished,\n    restart,\n  } = useSession(SESSION);\n  const isOver = state.steps.every(isDone);\n  const canReplay = isOver && isFinished && !isAutoplaying;\n  const announcement = announcementOf(state.steps);\n\n  function changeOpen(isOpen: boolean) {\n    act({ type: 'open', isOpen });\n  }\n\n  useKeptHeight(ref);\n\n  return (\n    <div\n      ref={ref}\n      className='flex min-h-[22rem] w-full max-w-[30rem] flex-col gap-2'\n    >\n      <ClockGate isActive={isInView}>\n        <TaskChecklist\n          title={TITLE}\n          agent={RESEARCHER}\n          steps={state.steps}\n          isOpen={state.isOpen}\n          onOpenChange={changeOpen}\n          isTogglePressed={pressed === 'toggle'}\n        />\n      </ClockGate>\n      <div className='flex h-7 items-center justify-end'>\n        <ReplayButton isShown={canReplay} onReplay={restart} />\n      </div>\n      <LiveRegion message={spokenUnless(isAutoplaying, announcement)} />\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/agent/agent-11.tsx"
    },
    {
      "path": "registry/ndeko/components/agent/_parts/agent-tile.tsx",
      "content": "import { seriesColorOf, type SeriesSlot } from '@/registry/ndeko/lib/agent-run';\nimport { cn } from '@/registry/ndeko/lib/utils';\nimport { MeshTile } from '@/registry/ndeko/ui/mesh-tile';\n\nconst SIZE_CLASS = {\n  sm: 'size-5 rounded-[5px]',\n  md: 'size-6 rounded-md',\n  lg: 'size-8 rounded-lg',\n} as const;\n\nexport type AgentTileSize = keyof typeof SIZE_CLASS;\n\ntype AgentTileProps = {\n  name: string;\n  series: SeriesSlot;\n  size?: AgentTileSize;\n  className?: string;\n};\n\n/** The picture of an agent: a gradient drawn from its name, in its series colour. Its name always sits beside it. */\nexport function AgentTile({\n  name,\n  series,\n  size = 'md',\n  className,\n}: AgentTileProps) {\n  const color = seriesColorOf(series);\n  const sizeClass = SIZE_CLASS[size];\n\n  return (\n    <MeshTile seed={name} color={color} className={cn(sizeClass, className)} />\n  );\n}\n",
      "type": "registry:component",
      "target": "components/agent/_parts/agent-tile.tsx"
    },
    {
      "path": "registry/ndeko/components/agent/_parts/crew.ts",
      "content": "import type { RunAgent } from '@/registry/ndeko/lib/agent-run';\n\nexport const RUN_TITLE = 'Q3 vendor review for Brightmoor Supply';\n\nexport const PLANNER: RunAgent = {\n  id: 'planner',\n  name: 'Planner',\n  series: 1,\n  status: 'idle',\n  role: 'Lead',\n  model: 'Claude Opus',\n};\n\nexport const RESEARCHER: RunAgent = {\n  id: 'researcher',\n  name: 'Researcher',\n  series: 2,\n  status: 'idle',\n  model: 'Claude Sonnet',\n  parentId: 'planner',\n};\n\nexport const WRITER: RunAgent = {\n  id: 'writer',\n  name: 'Writer',\n  series: 3,\n  status: 'idle',\n  model: 'Claude Haiku',\n  parentId: 'planner',\n};\n\nexport const REVIEWER: RunAgent = {\n  id: 'reviewer',\n  name: 'Reviewer',\n  series: 4,\n  status: 'idle',\n  model: 'Claude Sonnet',\n  parentId: 'planner',\n};\n\nexport const CODER: RunAgent = {\n  id: 'coder',\n  name: 'Coder',\n  series: 5,\n  status: 'idle',\n  model: 'Claude Opus',\n  parentId: 'planner',\n};\n\nexport const CREW: RunAgent[] = [PLANNER, RESEARCHER, WRITER, REVIEWER, CODER];\n\nexport const PAGE_ERROR = 'The pricing page refused the reader (403)';\n",
      "type": "registry:component",
      "target": "components/agent/_parts/crew.ts"
    },
    {
      "path": "registry/ndeko/components/agent/_parts/use-open-state.ts",
      "content": "'use client';\n\nimport { useState } from 'react';\n\ntype OpenStateOptions = {\n  /** When given, the parent owns the state. */\n  isOpen?: boolean;\n  defaultOpen: boolean;\n  onOpenChange?: (isOpen: boolean) => void;\n};\n\nfunction currentOpenOf(isOpen: boolean | undefined, ownOpen: boolean) {\n  if (isOpen === undefined) {\n    return ownOpen;\n  }\n  return isOpen;\n}\n\nexport function useOpenState({\n  isOpen,\n  defaultOpen,\n  onOpenChange,\n}: OpenStateOptions) {\n  const [ownOpen, setOwnOpen] = useState(defaultOpen);\n  const current = currentOpenOf(isOpen, ownOpen);\n\n  function toggle() {\n    const next = !current;\n    setOwnOpen(next);\n    onOpenChange?.(next);\n  }\n\n  return { isOpen: current, toggle };\n}\n",
      "type": "registry:component",
      "target": "components/agent/_parts/use-open-state.ts"
    }
  ],
  "categories": [
    "components",
    "agent"
  ],
  "type": "registry:component"
}