{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "answer-06",
  "title": "Artifact canvas",
  "description": "A chat beside the document it made, split by a handle you drag or move with the arrows, with versions that crossfade, changes marked against the last one, Copy and Apply.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://ui.ndekocode.dev/r/animated-size.json",
    "https://ui.ndekocode.dev/r/brand-logos.json",
    "https://ui.ndekocode.dev/r/brand-tile.json",
    "https://ui.ndekocode.dev/r/button.json",
    "https://ui.ndekocode.dev/r/chat-motion.json",
    "https://ui.ndekocode.dev/r/copy-button.json",
    "https://ui.ndekocode.dev/r/icons.json",
    "https://ui.ndekocode.dev/r/live-region.json",
    "https://ui.ndekocode.dev/r/morph-button.json",
    "https://ui.ndekocode.dev/r/switch.json",
    "https://ui.ndekocode.dev/r/theme.json",
    "https://ui.ndekocode.dev/r/toggle-group.json",
    "https://ui.ndekocode.dev/r/use-prefers-reduced-motion.json",
    "https://ui.ndekocode.dev/r/use-timeouts.json",
    "https://ui.ndekocode.dev/r/utils.json"
  ],
  "files": [
    {
      "path": "registry/ndeko/components/answer/answer-06.tsx",
      "content": "'use client';\n\nimport { AnimatePresence, motion } from 'motion/react';\nimport { useId, useRef, useState } from 'react';\n\nimport { usePrefersReducedMotion } from '@/registry/ndeko/hooks/use-prefers-reduced-motion';\nimport { useTimeouts } from '@/registry/ndeko/hooks/use-timeouts';\nimport {\n  CheckIcon,\n  FileTextIcon,\n  GripVerticalIcon,\n} from '@/registry/ndeko/lib/icons';\nimport { cn } from '@/registry/ndeko/lib/utils';\nimport { AnimatedSize } from '@/registry/ndeko/ui/animated-size';\nimport { Button } from '@/registry/ndeko/ui/button';\nimport { Fold, Swap } from '@/registry/ndeko/ui/chat-motion';\nimport { CopyButton } from '@/registry/ndeko/ui/copy-button';\nimport { LiveRegion } from '@/registry/ndeko/ui/live-region';\nimport { MorphButton, type MorphState } from '@/registry/ndeko/ui/morph-button';\nimport { Switch } from '@/registry/ndeko/ui/switch';\nimport { ToggleGroup, ToggleGroupItem } from '@/registry/ndeko/ui/toggle-group';\n\nimport { LabTile } from './_parts/lab-tile';\nimport { useContainerWidth } from './_parts/use-container-width';\n\nexport type ArtifactVersion = {\n  id: string;\n  label: string;\n  lines: string[];\n};\n\nexport type DiffLine = {\n  key: string;\n  kind: 'same' | 'added' | 'removed';\n  text: string;\n};\n\n/** Below this width of the piece, the panel opens under the chat instead of beside it. */\nconst SPLIT_MIN_WIDTH = 640;\n\nconst CHAT_MIN_PX = 200;\n\nconst PANEL_MIN_PX = 300;\n\nconst DEFAULT_PANEL_PERCENT = 58;\n\nconst KEY_STEP = 2;\n\nconst SHIFT_STEP = 10;\n\nconst APPLYING_MS = 800;\n\nconst THUMB =\n  'bg-card shadow-[0_1px_2px_rgb(0_0_0/0.08)] hairline-ring ring-foreground/10 dark:bg-foreground/[0.09] dark:ring-foreground/15';\n\nfunction emptyTable(rows: number, columns: number) {\n  const table: number[][] = [];\n  for (let row = 0; row <= rows; row += 1) {\n    const width = columns + 1;\n    const cells = Array.from({ length: width }, () => 0);\n    table.push(cells);\n  }\n  return table;\n}\n\nfunction commonLengths(before: string[], after: string[]) {\n  const table = emptyTable(before.length, after.length);\n  for (let row = before.length - 1; row >= 0; row -= 1) {\n    for (let column = after.length - 1; column >= 0; column -= 1) {\n      const isSame = before[row] === after[column];\n      if (isSame) {\n        const diagonal = table[row + 1][column + 1];\n        table[row][column] = diagonal + 1;\n        continue;\n      }\n      const below = table[row + 1][column];\n      const right = table[row][column + 1];\n      table[row][column] = Math.max(below, right);\n    }\n  }\n  return table;\n}\n\nexport function lineDiffOf(before: string[], after: string[]): DiffLine[] {\n  const table = commonLengths(before, after);\n  const lines: DiffLine[] = [];\n  let row = 0;\n  let column = 0;\n  while (row < before.length && column < after.length) {\n    const isSame = before[row] === after[column];\n    if (isSame) {\n      lines.push({\n        key: `s${row}-${column}`,\n        kind: 'same',\n        text: after[column],\n      });\n      row += 1;\n      column += 1;\n      continue;\n    }\n    const keptByRemoving = table[row + 1][column];\n    const keptByAdding = table[row][column + 1];\n    const removesFirst = keptByRemoving >= keptByAdding;\n    if (removesFirst) {\n      lines.push({ key: `r${row}`, kind: 'removed', text: before[row] });\n      row += 1;\n      continue;\n    }\n    lines.push({ key: `a${column}`, kind: 'added', text: after[column] });\n    column += 1;\n  }\n  while (row < before.length) {\n    lines.push({ key: `r${row}`, kind: 'removed', text: before[row] });\n    row += 1;\n  }\n  while (column < after.length) {\n    lines.push({ key: `a${column}`, kind: 'added', text: after[column] });\n    column += 1;\n  }\n  return lines;\n}\n\nfunction plainLinesOf(version: ArtifactVersion): DiffLine[] {\n  return version.lines.map((text, index) => ({\n    key: `p${index}`,\n    kind: 'same',\n    text,\n  }));\n}\n\nconst HEADING = /^#\\s+/;\n\nfunction isHeading(text: string) {\n  return HEADING.test(text);\n}\n\nfunction headingTextOf(text: string) {\n  return text.replace(HEADING, '');\n}\n\nconst LINE_TONE: Record<DiffLine['kind'], string> = {\n  same: '',\n  added: 'bg-success/10',\n  removed: 'text-muted-foreground line-through decoration-muted-foreground/60',\n};\n\nconst GUTTER: Record<DiffLine['kind'], string> = {\n  same: '',\n  added: '+',\n  removed: '−',\n};\n\nconst GUTTER_TONE: Record<DiffLine['kind'], string> = {\n  same: '',\n  added: 'text-success-foreground',\n  removed: 'text-destructive-ink',\n};\n\nconst SPOKEN_KIND: Record<DiffLine['kind'], string> = {\n  same: '',\n  added: 'Added: ',\n  removed: 'Removed: ',\n};\n\nfunction DocumentLine({\n  line,\n  showsGutter,\n}: {\n  line: DiffLine;\n  showsGutter: boolean;\n}) {\n  const heading = isHeading(line.text);\n\n  return (\n    <li\n      className={cn(\n        'flex gap-2 rounded-md px-1.5 py-0.5',\n        LINE_TONE[line.kind]\n      )}\n    >\n      {showsGutter && (\n        <span\n          aria-hidden='true'\n          className={cn(\n            'w-3 shrink-0 text-center font-mono text-xs leading-6',\n            GUTTER_TONE[line.kind]\n          )}\n        >\n          {GUTTER[line.kind]}\n        </span>\n      )}\n      <span className='sr-only'>{SPOKEN_KIND[line.kind]}</span>\n      {heading && (\n        <span className='min-w-0 text-[15px] leading-6 font-semibold text-balance'>\n          {headingTextOf(line.text)}\n        </span>\n      )}\n      {!heading && (\n        <span className='min-w-0 text-[13px] leading-6'>{line.text}</span>\n      )}\n    </li>\n  );\n}\n\nfunction applyStateOf(\n  versionId: string,\n  appliedId: string | null,\n  applyingId: string | null\n): MorphState {\n  if (applyingId === versionId) {\n    return 'busy';\n  }\n  if (appliedId === versionId) {\n    return 'done';\n  }\n  return 'idle';\n}\n\nconst APPLY_LABELS: Record<MorphState, string> = {\n  idle: 'Apply',\n  busy: 'Applying',\n  done: 'Applied',\n};\n\nconst CONTENT_SWAP = {\n  initial: { opacity: 0, filter: 'blur(2px)' },\n  animate: { opacity: 1, filter: 'blur(0px)' },\n  exit: { opacity: 0, filter: 'blur(2px)' },\n  transition: { duration: 0.2, ease: [0.23, 1, 0.32, 1] },\n} as const;\n\nconst CONTENT_FADE = {\n  initial: { opacity: 0 },\n  animate: { opacity: 1 },\n  exit: { opacity: 0 },\n  transition: { duration: 0.15 },\n} as const;\n\nfunction contentMotionOf(shouldReduceMotion: boolean | null) {\n  if (shouldReduceMotion) {\n    return CONTENT_FADE;\n  }\n  return CONTENT_SWAP;\n}\n\nexport type ArtifactPanelProps = {\n  title: string;\n  versions: ArtifactVersion[];\n  versionId: string;\n  onVersionChange: (versionId: string) => void;\n  appliedId: string | null;\n  applyingId: string | null;\n  onApply: (versionId: string) => void;\n  showsChanges: boolean;\n  onShowsChangesChange: (showsChanges: boolean) => void;\n  id?: string;\n  className?: string;\n};\n\nexport function ArtifactPanel({\n  title,\n  versions,\n  versionId,\n  onVersionChange,\n  appliedId,\n  applyingId,\n  onApply,\n  showsChanges,\n  onShowsChangesChange,\n  id,\n  className,\n}: ArtifactPanelProps) {\n  const shouldReduceMotion = usePrefersReducedMotion();\n  const contentMotion = contentMotionOf(shouldReduceMotion);\n  const index = versions.findIndex((version) => version.id === versionId);\n  const version = versions[index] ?? versions[0];\n  const previous = versions[index - 1];\n  const hasPrevious = previous !== undefined;\n  const isDiffing = showsChanges && hasPrevious;\n  const lines = linesOf(version, previous, isDiffing);\n  const applyState = applyStateOf(version.id, appliedId, applyingId);\n  const contentKey = `${version.id}-${String(isDiffing)}`;\n  const copyText = version.lines.join('\\n');\n\n  function changeVersion(values: unknown[]) {\n    const [next] = values;\n    const match = versions.find((candidate) => candidate.id === next);\n    if (match) {\n      onVersionChange(match.id);\n    }\n  }\n\n  return (\n    <section\n      id={id}\n      aria-label={`${title}, ${version.label}`}\n      className={cn('flex min-w-0 flex-col gap-3', className)}\n    >\n      <header className='flex flex-col gap-2.5'>\n        <div className='flex min-w-0 items-center gap-2'>\n          <span className='keycap-3d text-muted-foreground inline-flex size-6 shrink-0 items-center justify-center rounded-md'>\n            <FileTextIcon className='size-3.5' />\n          </span>\n          <h3 className='min-w-0 flex-1 truncate font-mono text-[13px]'>\n            {title}\n          </h3>\n          <CopyButton text={copyText} label={`Copy ${version.label}`} />\n          <MorphButton\n            type='button'\n            size='sm'\n            state={applyState}\n            labels={APPLY_LABELS}\n            onClick={() => onApply(version.id)}\n          />\n        </div>\n        <div className='flex flex-wrap items-center justify-between gap-2'>\n          <ToggleGroup\n            aria-label='Versions'\n            value={[version.id]}\n            onValueChange={changeVersion}\n            travel\n            size='sm'\n            indicatorClassName={THUMB}\n            className='bg-foreground/[0.05] rounded-lg p-0.5'\n          >\n            {versions.map((candidate) => {\n              const isApplied = candidate.id === appliedId;\n              return (\n                <ToggleGroupItem\n                  key={candidate.id}\n                  value={candidate.id}\n                  aria-label={versionLabelOf(candidate, isApplied)}\n                  className='h-6 gap-1 rounded-md px-2.5 text-xs'\n                >\n                  {candidate.label}\n                  <AnimatePresence initial={false}>\n                    {isApplied && (\n                      <motion.span\n                        key='applied'\n                        className='text-success-foreground inline-flex'\n                        initial={{ opacity: 0, scale: 0.6 }}\n                        animate={{ opacity: 1, scale: 1 }}\n                        exit={{ opacity: 0, scale: 0.6 }}\n                        transition={{ duration: 0.15 }}\n                      >\n                        <CheckIcon className='size-3' strokeWidth={2.5} />\n                      </motion.span>\n                    )}\n                  </AnimatePresence>\n                </ToggleGroupItem>\n              );\n            })}\n          </ToggleGroup>\n          <label className='text-muted-foreground flex cursor-pointer items-center gap-2 text-xs'>\n            Changes\n            <Switch\n              checked={isDiffing}\n              disabled={!hasPrevious}\n              onCheckedChange={(checked) => onShowsChangesChange(checked)}\n            />\n          </label>\n        </div>\n      </header>\n      <AnimatedSize>\n        <div className='relative'>\n          <AnimatePresence initial={false} mode='popLayout'>\n            <motion.ol\n              key={contentKey}\n              className='bg-foreground/[0.025] dark:bg-foreground/[0.04] flex flex-col rounded-lg p-2'\n              {...contentMotion}\n            >\n              {lines.map((line) => (\n                <DocumentLine\n                  key={line.key}\n                  line={line}\n                  showsGutter={isDiffing}\n                />\n              ))}\n            </motion.ol>\n          </AnimatePresence>\n        </div>\n      </AnimatedSize>\n      {!hasPrevious && (\n        <p className='text-muted-foreground text-xs'>\n          The first version has nothing to compare with\n        </p>\n      )}\n    </section>\n  );\n}\n\nfunction linesOf(\n  version: ArtifactVersion,\n  previous: ArtifactVersion | undefined,\n  isDiffing: boolean\n) {\n  if (!isDiffing) {\n    return plainLinesOf(version);\n  }\n  if (!previous) {\n    return plainLinesOf(version);\n  }\n  return lineDiffOf(previous.lines, version.lines);\n}\n\nfunction versionLabelOf(version: ArtifactVersion, isApplied: boolean) {\n  if (isApplied) {\n    return `${version.label}, applied`;\n  }\n  return version.label;\n}\n\ntype Bounds = { min: number; max: number };\n\nfunction boundsOf(width: number): Bounds {\n  const safeWidth = Math.max(width, 1);\n  const panelMin = (PANEL_MIN_PX / safeWidth) * 100;\n  const chatMin = (CHAT_MIN_PX / safeWidth) * 100;\n  const panelMax = 100 - chatMin;\n  const min = Math.ceil(panelMin);\n  const max = Math.floor(panelMax);\n  return { min, max: Math.max(min, max) };\n}\n\nfunction clampPercent(percent: number, bounds: Bounds) {\n  const capped = Math.min(bounds.max, percent);\n  return Math.max(bounds.min, capped);\n}\n\nfunction stepOf(event: React.KeyboardEvent) {\n  if (event.shiftKey) {\n    return SHIFT_STEP;\n  }\n  return KEY_STEP;\n}\n\ntype ResizeHandleProps = {\n  percent: number;\n  bounds: Bounds;\n  panelId: string;\n  isDragging: boolean;\n  onResize: (percent: number) => void;\n  onResizeEnd: () => void;\n  onDragStart: () => void;\n  onDragMove: (clientX: number) => void;\n  onReset: () => void;\n};\n\n/** A separator in the window splitter pattern: its value is the width of the artifact panel. */\nfunction ResizeHandle({\n  percent,\n  bounds,\n  panelId,\n  isDragging,\n  onResize,\n  onResizeEnd,\n  onDragStart,\n  onDragMove,\n  onReset,\n}: ResizeHandleProps) {\n  const value = Math.round(percent);\n\n  function handleKeys(event: React.KeyboardEvent<HTMLDivElement>) {\n    const step = stepOf(event);\n    if (event.key === 'ArrowLeft') {\n      event.preventDefault();\n      const wider = percent + step;\n      onResize(wider);\n      return;\n    }\n    if (event.key === 'ArrowRight') {\n      event.preventDefault();\n      const narrower = percent - step;\n      onResize(narrower);\n      return;\n    }\n    if (event.key === 'Home') {\n      event.preventDefault();\n      onResize(bounds.min);\n      return;\n    }\n    if (event.key === 'End') {\n      event.preventDefault();\n      onResize(bounds.max);\n    }\n  }\n\n  function startDrag(event: React.PointerEvent<HTMLDivElement>) {\n    event.currentTarget.setPointerCapture(event.pointerId);\n    onDragStart();\n  }\n\n  function moveDrag(event: React.PointerEvent<HTMLDivElement>) {\n    if (!isDragging) {\n      return;\n    }\n    onDragMove(event.clientX);\n  }\n\n  function endDrag(event: React.PointerEvent<HTMLDivElement>) {\n    const hasCapture = event.currentTarget.hasPointerCapture(event.pointerId);\n    if (hasCapture) {\n      event.currentTarget.releasePointerCapture(event.pointerId);\n    }\n    onResizeEnd();\n  }\n\n  return (\n    <div\n      role='separator'\n      tabIndex={0}\n      aria-orientation='vertical'\n      aria-controls={panelId}\n      aria-label='Resize the artifact panel'\n      aria-valuenow={value}\n      aria-valuemin={bounds.min}\n      aria-valuemax={bounds.max}\n      aria-valuetext={`Artifact panel ${value}% wide`}\n      onKeyDown={handleKeys}\n      onKeyUp={onResizeEnd}\n      onPointerDown={startDrag}\n      onPointerMove={moveDrag}\n      onPointerUp={endDrag}\n      onPointerCancel={endDrag}\n      onDoubleClick={onReset}\n      className='group/handle focus-visible:ring-ring/25 relative flex w-3 shrink-0 cursor-col-resize touch-none items-center justify-center rounded-full outline-none focus-visible:ring-[3px]'\n    >\n      <span\n        aria-hidden='true'\n        className={cn(\n          'bg-border group-hover/handle:bg-foreground/25 absolute inset-y-2 w-px transition-colors duration-150',\n          isDragging && 'bg-primary'\n        )}\n      />\n      <span\n        aria-hidden='true'\n        className={cn(\n          'keycap-3d text-muted-foreground relative flex h-7 w-3.5 items-center justify-center rounded-full transition-[scale] duration-150 ease-(--ease-out-strong)',\n          isDragging && 'scale-110'\n        )}\n      >\n        <GripVerticalIcon className='size-3' />\n      </span>\n    </div>\n  );\n}\n\ntype ChatTurn =\n  | { id: string; role: 'user'; text: string }\n  | { id: string; role: 'assistant'; text: string; versionId: string };\n\ntype ChatColumnProps = {\n  turns: ChatTurn[];\n  versions: ArtifactVersion[];\n  title: string;\n  versionId: string;\n  onOpenVersion: (versionId: string) => void;\n  className?: string;\n};\n\nfunction ChatColumn({\n  turns,\n  versions,\n  title,\n  versionId,\n  onOpenVersion,\n  className,\n}: ChatColumnProps) {\n  return (\n    <ol\n      aria-label='Conversation'\n      className={cn('flex min-w-0 flex-col gap-3', className)}\n    >\n      {turns.map((turn) => {\n        if (turn.role === 'user') {\n          return (\n            <li key={turn.id} className='flex justify-end'>\n              <p className='bg-foreground/[0.05] dark:bg-foreground/[0.07] max-w-[90%] rounded-xl px-3 py-1.5 text-[13px] leading-relaxed'>\n                {turn.text}\n              </p>\n            </li>\n          );\n        }\n        const version = versions.find(\n          (candidate) => candidate.id === turn.versionId\n        );\n        const isOpen = turn.versionId === versionId;\n        return (\n          <li key={turn.id} className='flex items-start gap-2'>\n            <LabTile lab='anthropic' size='sm' className='mt-1' />\n            <div className='flex min-w-0 flex-col gap-1.5'>\n              <p className='text-[13px] leading-relaxed'>{turn.text}</p>\n              {version && (\n                <button\n                  type='button'\n                  aria-pressed={isOpen}\n                  onClick={() => onOpenVersion(version.id)}\n                  className={cn(\n                    'focus-visible:ring-ring/25 flex h-7 w-fit max-w-full cursor-pointer items-center gap-1.5 rounded-lg px-2 text-xs transition-colors duration-150 outline-none focus-visible:ring-[3px]',\n                    chipToneOf(isOpen)\n                  )}\n                >\n                  <FileTextIcon className='size-3.5 shrink-0' />\n                  <span className='min-w-0 truncate font-mono'>{title}</span>\n                  <span className='text-muted-foreground shrink-0'>\n                    {version.label}\n                  </span>\n                </button>\n              )}\n            </div>\n          </li>\n        );\n      })}\n    </ol>\n  );\n}\n\nfunction chipToneOf(isOpen: boolean) {\n  if (isOpen) {\n    return 'bg-primary/10 text-primary-ink';\n  }\n  return 'bg-foreground/[0.05] hover:bg-foreground/[0.08]';\n}\n\nfunction sheetLabelOf(isOpen: boolean) {\n  if (isOpen) {\n    return 'Hide artifact';\n  }\n  return 'Show artifact';\n}\n\nconst TITLE = 'pallet-fee-notice.md';\n\nconst VERSIONS: ArtifactVersion[] = [\n  {\n    id: 'v1',\n    label: 'v1',\n    lines: [\n      '# A new pallet fee from 1 October',\n      'Dear customer,',\n      'From 1 October, Brightmoor bills bulk orders a flat fee of 18.40 EUR per pallet.',\n      'This replaces the tiered pricing that applied to all orders until now.',\n      'We made this change to keep bulk pricing simple and predictable.',\n      'If you have questions, reply to this email or call your account manager.',\n      'The Brightmoor team',\n    ],\n  },\n  {\n    id: 'v2',\n    label: 'v2',\n    lines: [\n      '# A new pallet fee from 1 October',\n      'Dear customer,',\n      'From 1 October, orders of 40 pallets or more are billed a flat 18.40 EUR per pallet.',\n      'Orders under 40 pallets keep the tiered rate, from 15.90 EUR.',\n      'Questions? Reply to this email or call your account manager.',\n      'The Brightmoor team',\n    ],\n  },\n  {\n    id: 'v3',\n    label: 'v3',\n    lines: [\n      '# A new pallet fee from 1 October',\n      'Dear customer,',\n      'As announced on 2 September, orders of 40 pallets or more are billed a flat 18.40 EUR per pallet from 1 October.',\n      'Orders under 40 pallets keep the tiered rate, from 15.90 EUR.',\n      'Questions? Reply to this email or call your account manager.',\n      'The Brightmoor team',\n    ],\n  },\n];\n\nconst TURNS: ChatTurn[] = [\n  {\n    id: 'ask-1',\n    role: 'user',\n    text: 'Draft a short notice for customers about the pallet fee change.',\n  },\n  {\n    id: 'draft-1',\n    role: 'assistant',\n    text: 'Here is a first draft.',\n    versionId: 'v1',\n  },\n  {\n    id: 'ask-2',\n    role: 'user',\n    text: 'Shorter, and say which orders keep the old rate.',\n  },\n  {\n    id: 'draft-2',\n    role: 'assistant',\n    text: 'Shorter now, and it names the orders that keep the tiers.',\n    versionId: 'v2',\n  },\n  {\n    id: 'ask-3',\n    role: 'user',\n    text: 'Mention when we announced it.',\n  },\n  {\n    id: 'draft-3',\n    role: 'assistant',\n    text: 'Added the announcement date to the first line.',\n    versionId: 'v3',\n  },\n];\n\nfunction versionLabelFor(versionId: string) {\n  const version = VERSIONS.find((candidate) => candidate.id === versionId);\n  return version?.label ?? versionId;\n}\n\nexport default function Answer06() {\n  const [versionId, setVersionId] = useState('v3');\n  const [showsChanges, setShowsChanges] = useState(true);\n  const [appliedId, setAppliedId] = useState<string | null>(null);\n  const [applyingId, setApplyingId] = useState<string | null>(null);\n  const [panelPercent, setPanelPercent] = useState(DEFAULT_PANEL_PERCENT);\n  const [isDragging, setIsDragging] = useState(false);\n  const [isSheetOpen, setIsSheetOpen] = useState(false);\n  const [notice, setNotice] = useState('');\n  const [root, setRoot] = useState<HTMLDivElement | null>(null);\n  const splitRef = useRef<HTMLDivElement>(null);\n  const panelId = useId();\n  const { schedule } = useTimeouts();\n  const width = useContainerWidth(root, 880);\n  const isSplit = width >= SPLIT_MIN_WIDTH;\n  const bounds = boundsOf(width);\n  const percent = clampPercent(panelPercent, bounds);\n  const panelStyle = { width: `${percent}%` };\n\n  function openVersion(nextId: string) {\n    setVersionId(nextId);\n    setIsSheetOpen(true);\n    setNotice(`Showing ${versionLabelFor(nextId)}`);\n  }\n\n  function changeVersion(nextId: string) {\n    setVersionId(nextId);\n    setNotice(`Showing ${versionLabelFor(nextId)}`);\n  }\n\n  function changeShowsChanges(next: boolean) {\n    setShowsChanges(next);\n    if (next) {\n      setNotice('Changes against the previous version are marked');\n      return;\n    }\n    setNotice('Changes hidden');\n  }\n\n  function apply(nextId: string) {\n    setApplyingId(nextId);\n    schedule(() => {\n      setApplyingId(null);\n      setAppliedId(nextId);\n      setNotice(`Applied ${versionLabelFor(nextId)}`);\n    }, APPLYING_MS);\n  }\n\n  function resize(next: number) {\n    const clamped = clampPercent(next, bounds);\n    setPanelPercent(clamped);\n  }\n\n  function announceSize() {\n    const rounded = Math.round(percent);\n    setNotice(`Artifact panel ${rounded}% wide`);\n  }\n\n  function startDrag() {\n    setIsDragging(true);\n  }\n\n  function moveDrag(clientX: number) {\n    const split = splitRef.current;\n    if (!split) {\n      return;\n    }\n    const rect = split.getBoundingClientRect();\n    const fromRight = rect.right - clientX;\n    const share = (fromRight / rect.width) * 100;\n    resize(share);\n  }\n\n  function endResize() {\n    setIsDragging(false);\n    announceSize();\n  }\n\n  function resetSize() {\n    resize(DEFAULT_PANEL_PERCENT);\n  }\n\n  const panel = (\n    <ArtifactPanel\n      id={panelId}\n      title={TITLE}\n      versions={VERSIONS}\n      versionId={versionId}\n      onVersionChange={changeVersion}\n      appliedId={appliedId}\n      applyingId={applyingId}\n      onApply={apply}\n      showsChanges={showsChanges}\n      onShowsChangesChange={changeShowsChanges}\n    />\n  );\n\n  return (\n    <div\n      ref={setRoot}\n      className={cn(\n        'bg-card hairline-ring ring-border w-full max-w-[60rem] rounded-xl p-3 shadow-[0_1px_2px_rgb(0_0_0/0.04)]',\n        isDragging && 'select-none'\n      )}\n    >\n      {isSplit && (\n        <div ref={splitRef} className='flex h-[28rem] gap-1'>\n          <ChatColumn\n            turns={TURNS}\n            versions={VERSIONS}\n            title={TITLE}\n            versionId={versionId}\n            onOpenVersion={openVersion}\n            className='min-h-0 flex-1 overflow-y-auto p-2'\n          />\n          <ResizeHandle\n            percent={percent}\n            bounds={bounds}\n            panelId={panelId}\n            isDragging={isDragging}\n            onResize={resize}\n            onResizeEnd={endResize}\n            onDragStart={startDrag}\n            onDragMove={moveDrag}\n            onReset={resetSize}\n          />\n          <div\n            style={panelStyle}\n            className='bg-foreground/[0.02] dark:bg-foreground/[0.035] min-h-0 shrink-0 overflow-y-auto rounded-lg p-3'\n          >\n            {panel}\n          </div>\n        </div>\n      )}\n      {!isSplit && (\n        <div className='flex flex-col gap-3'>\n          <ChatColumn\n            turns={TURNS}\n            versions={VERSIONS}\n            title={TITLE}\n            versionId={versionId}\n            onOpenVersion={openVersion}\n            className='p-1'\n          />\n          <Button\n            type='button'\n            variant='outline'\n            size='sm'\n            aria-expanded={isSheetOpen}\n            aria-controls={panelId}\n            onClick={() => setIsSheetOpen((current) => !current)}\n            className='self-center'\n          >\n            <FileTextIcon />\n            <Swap swapKey={sheetLabelOf(isSheetOpen)}>\n              {sheetLabelOf(isSheetOpen)}\n            </Swap>\n          </Button>\n          <Fold open={isSheetOpen}>\n            <div className='surface-raised flex flex-col gap-3 rounded-2xl p-3 pt-2'>\n              <span\n                aria-hidden='true'\n                className='bg-foreground/15 mx-auto h-1 w-9 rounded-full'\n              />\n              {panel}\n            </div>\n          </Fold>\n        </div>\n      )}\n      <LiveRegion message={notice} />\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/answer/answer-06.tsx"
    },
    {
      "path": "registry/ndeko/components/answer/_parts/lab-tile.tsx",
      "content": "import { BrandTile, type BrandTileSize } from '@/registry/ndeko/ui/brand-tile';\n\nimport { labOf, type LabId } from './labs';\n\ntype LabTileProps = {\n  lab: LabId;\n  size?: BrandTileSize;\n  className?: string;\n};\n\nexport function LabTile({ lab, size = 'md', className }: LabTileProps) {\n  const entry = labOf(lab);\n\n  return <BrandTile brand={entry.brand} size={size} className={className} />;\n}\n",
      "type": "registry:component",
      "target": "components/answer/_parts/lab-tile.tsx"
    },
    {
      "path": "registry/ndeko/components/answer/_parts/labs.ts",
      "content": "import type { BrandSlug } from '@/registry/ndeko/lib/brand-logos';\n\nexport type LabId = 'anthropic' | 'openai' | 'google';\n\nexport type Lab = {\n  id: LabId;\n  name: string;\n  brand: BrandSlug;\n};\n\nexport const LABS: Lab[] = [\n  { id: 'anthropic', name: 'Anthropic', brand: 'claude' },\n  { id: 'openai', name: 'OpenAI', brand: 'openai' },\n  { id: 'google', name: 'Google', brand: 'gemini' },\n];\n\nconst FALLBACK_LAB = LABS[0];\n\nexport function labOf(id: LabId) {\n  const lab = LABS.find((candidate) => candidate.id === id);\n  return lab ?? FALLBACK_LAB;\n}\n",
      "type": "registry:component",
      "target": "components/answer/_parts/labs.ts"
    },
    {
      "path": "registry/ndeko/components/answer/_parts/use-container-width.ts",
      "content": "'use client';\n\nimport { useLayoutEffect, useState } from 'react';\n\n/**\n * The width of an element as it resizes. A piece in a narrow column of a wide window switches layout\n * by the room it has, which the viewport cannot tell.\n */\nexport function useContainerWidth(\n  target: HTMLElement | null,\n  fallback: number\n) {\n  const [width, setWidth] = useState(fallback);\n\n  useLayoutEffect(() => {\n    if (!target) {\n      return;\n    }\n    const observer = new ResizeObserver(([entry]) => {\n      const box = entry.contentBoxSize[0];\n      setWidth(box.inlineSize);\n    });\n    observer.observe(target);\n    return () => observer.disconnect();\n  }, [target]);\n\n  return width;\n}\n",
      "type": "registry:component",
      "target": "components/answer/_parts/use-container-width.ts"
    }
  ],
  "categories": [
    "components",
    "answer"
  ],
  "type": "registry:component"
}