{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "answer-08",
  "title": "Query result",
  "description": "A query an agent ran, in Result and Query tabs under a travelling indicator: rows arrive under the header with the row count and run time, the SQL is highlighted, and an error names the line it points at.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://ui.ndekocode.dev/r/badge.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/demonstration.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/motion-number.json",
    "https://ui.ndekocode.dev/r/replay-button.json",
    "https://ui.ndekocode.dev/r/tabs.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",
    "https://ui.ndekocode.dev/r/words.json"
  ],
  "files": [
    {
      "path": "registry/ndeko/components/answer/answer-08.tsx",
      "content": "'use client';\n\nimport { AnimatePresence, motion } from 'motion/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 { spokenUnless } from '@/registry/ndeko/lib/demonstration';\nimport { LoaderIcon } from '@/registry/ndeko/lib/icons';\nimport { cn } from '@/registry/ndeko/lib/utils';\nimport { countOf, nounOf } from '@/registry/ndeko/lib/words';\nimport { Badge } from '@/registry/ndeko/ui/badge';\nimport { BrandTile } from '@/registry/ndeko/ui/brand-tile';\nimport { Button } from '@/registry/ndeko/ui/button';\nimport { Swap } from '@/registry/ndeko/ui/chat-motion';\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 { Tabs, TabsList, TabsPanel, TabsTab } from '@/registry/ndeko/ui/tabs';\n\nimport { CopyLabelButton } from './_parts/copy-label-button';\nimport { sqlTokensOf, type SqlTokenKind } from './_parts/sql';\n\nexport type QueryTab = 'result' | 'query';\n\nexport type QueryStatus = 'running' | 'done' | 'error';\n\nexport type QueryCell = string | number;\n\nexport type QueryError = {\n  message: string;\n  /** The line of the query the message points at, counted from 1. */\n  line: number;\n};\n\nexport type QueryRun = {\n  title: string;\n  sql: string;\n  status: QueryStatus;\n  columns: string[];\n  rows: QueryCell[][];\n  /** How long the query took, in milliseconds. */\n  ms?: number;\n  error?: QueryError;\n};\n\nconst TOKEN_TONE: Record<SqlTokenKind, string> = {\n  keyword: 'text-primary-ink',\n  function: 'text-info-foreground',\n  string: 'text-success-foreground',\n  number: 'text-warning-foreground',\n  comment: 'text-muted-foreground',\n  plain: 'text-foreground',\n};\n\nconst STATUS_BADGE: Record<\n  QueryStatus,\n  { label: string; variant: React.ComponentProps<typeof Badge>['variant'] }\n> = {\n  running: { label: 'Running', variant: 'info' },\n  done: { label: 'Done', variant: 'success' },\n  error: { label: 'Failed', variant: 'destructive' },\n};\n\nconst NUMBER_FORMATTER = new Intl.NumberFormat('en-US');\n\nconst PROGRESS_START = { scaleX: 0.05, opacity: 1 };\n\nconst PROGRESS_RUNNING = { scaleX: 0.85, opacity: 1 };\n\nconst PROGRESS_DONE = { scaleX: 1, opacity: 0 };\n\nconst NUDGE = [0, -4, 4, -2, 0];\n\nfunction isQueryTab(value: unknown): value is QueryTab {\n  return value === 'result' || value === 'query';\n}\n\nfunction formatCell(cell: QueryCell) {\n  if (typeof cell === 'number') {\n    return NUMBER_FORMATTER.format(cell);\n  }\n  return cell;\n}\n\nfunction csvCellOf(cell: QueryCell) {\n  const text = String(cell);\n  const needsQuotes = /[\",\\n]/.test(text);\n  if (!needsQuotes) {\n    return text;\n  }\n  const escaped = text.replaceAll('\"', '\"\"');\n  return `\"${escaped}\"`;\n}\n\nfunction csvOf(columns: string[], rows: QueryCell[][]) {\n  const lines = [columns.join(',')];\n  for (const row of rows) {\n    lines.push(row.map(csvCellOf).join(','));\n  }\n  return lines.join('\\n');\n}\n\nfunction alignOf(cell: QueryCell | undefined) {\n  if (typeof cell === 'number') {\n    return 'text-end';\n  }\n  return 'text-start';\n}\n\nfunction lineSurfaceOf(isErrorLine: boolean) {\n  if (isErrorLine) {\n    return 'bg-destructive/10';\n  }\n  return 'bg-transparent';\n}\n\nfunction lineNumberToneOf(isErrorLine: boolean) {\n  if (isErrorLine) {\n    return 'text-destructive-ink';\n  }\n  return 'text-muted-foreground';\n}\n\nfunction progressDurationOf(shouldReduceMotion: boolean | null) {\n  if (shouldReduceMotion) {\n    return 0;\n  }\n  return 1.4;\n}\n\nfunction RunLine({ isRunning }: { isRunning: boolean }) {\n  const shouldReduceMotion = usePrefersReducedMotion();\n  const duration = progressDurationOf(shouldReduceMotion);\n\n  return (\n    <AnimatePresence initial={false}>\n      {isRunning && (\n        <motion.span\n          key='running'\n          aria-hidden='true'\n          initial={PROGRESS_START}\n          animate={PROGRESS_RUNNING}\n          exit={{ ...PROGRESS_DONE, transition: { duration: 0.25 } }}\n          transition={{ duration, ease: [0.23, 1, 0.32, 1] }}\n          className='bg-primary absolute inset-x-0 top-0 z-10 h-0.5 origin-left'\n        />\n      )}\n    </AnimatePresence>\n  );\n}\n\ntype ResultTableProps = {\n  columns: string[];\n  rows: QueryCell[][];\n};\n\nfunction ResultTable({ columns, rows }: ResultTableProps) {\n  const firstRow = rows[0];\n\n  return (\n    <div className='bg-foreground/[0.03] dark:bg-foreground/[0.05] relative overflow-x-auto rounded-lg'>\n      <table className='w-full text-[13px]'>\n        <thead>\n          <tr>\n            {columns.map((column, index) => (\n              <th\n                key={column}\n                scope='col'\n                className={cn(\n                  'text-muted-foreground px-3 py-2 font-mono text-xs font-medium whitespace-nowrap',\n                  alignOf(firstRow?.[index])\n                )}\n              >\n                {column}\n              </th>\n            ))}\n          </tr>\n        </thead>\n        <tbody>\n          {rows.map((row, rowIndex) => (\n            <tr\n              key={rowIndex}\n              className='hairline-t border-border transition-[opacity,translate] duration-200 ease-(--ease-out-strong) starting:-translate-y-1 starting:opacity-0'\n            >\n              {row.map((cell, cellIndex) => (\n                <td\n                  key={cellIndex}\n                  className={cn(\n                    'px-3 py-2 whitespace-nowrap tabular-nums',\n                    alignOf(cell)\n                  )}\n                >\n                  {formatCell(cell)}\n                </td>\n              ))}\n            </tr>\n          ))}\n        </tbody>\n      </table>\n    </div>\n  );\n}\n\nfunction ResultFooter({ run }: { run: QueryRun }) {\n  const rowCount = run.rows.length;\n\n  return (\n    <p className='text-muted-foreground flex flex-wrap items-center gap-x-1.5 px-1 text-xs'>\n      <span className='tabular-nums'>\n        <MotionNumber value={rowCount} /> {nounOf(rowCount, 'row')}\n      </span>\n      <span aria-hidden='true'>·</span>\n      <span className='tabular-nums'>\n        <MotionNumber value={run.ms ?? 0} /> ms\n      </span>\n    </p>\n  );\n}\n\nfunction nudgeOf(shouldReduceMotion: boolean | null) {\n  if (shouldReduceMotion) {\n    return { x: 0 };\n  }\n  return { x: NUDGE };\n}\n\ntype ErrorNoticeProps = {\n  error: QueryError;\n  onShowLine: () => void;\n};\n\nfunction ErrorNotice({ error, onShowLine }: ErrorNoticeProps) {\n  const shouldReduceMotion = usePrefersReducedMotion();\n  const nudge = nudgeOf(shouldReduceMotion);\n\n  return (\n    <motion.div\n      initial={{ x: 0 }}\n      animate={nudge}\n      transition={{ duration: 0.3, ease: [0.23, 1, 0.32, 1] }}\n      className='bg-destructive/10 flex flex-col gap-2 rounded-lg p-3'\n    >\n      <p className='text-destructive-ink text-[13px] font-medium'>\n        The query failed on line {error.line}\n      </p>\n      <p className='text-destructive-ink font-mono text-xs leading-5 break-words'>\n        {error.message}\n      </p>\n      <div>\n        <Button type='button' variant='outline' size='sm' onClick={onShowLine}>\n          Show line {error.line}\n        </Button>\n      </div>\n    </motion.div>\n  );\n}\n\nfunction QueryCode({\n  sql,\n  errorLine,\n}: {\n  sql: string;\n  errorLine: number | undefined;\n}) {\n  const lines = sql.split('\\n');\n\n  return (\n    <div className='bg-foreground/[0.04] dark:bg-foreground/[0.06] relative overflow-x-auto rounded-lg py-2 font-mono text-xs leading-5'>\n      {lines.map((line, index) => {\n        const number = index + 1;\n        const isErrorLine = number === errorLine;\n        return (\n          <div\n            key={number}\n            className={cn(\n              'flex min-w-max transition-colors duration-200',\n              lineSurfaceOf(isErrorLine)\n            )}\n          >\n            <span\n              aria-hidden='true'\n              className={cn(\n                'w-9 shrink-0 pe-3 text-end tabular-nums select-none',\n                lineNumberToneOf(isErrorLine)\n              )}\n            >\n              {number}\n            </span>\n            <span className='pe-3 whitespace-pre'>\n              {sqlTokensOf(line).map((token, tokenIndex) => (\n                <span key={tokenIndex} className={TOKEN_TONE[token.kind]}>\n                  {token.text}\n                </span>\n              ))}\n              {isErrorLine && (\n                <span className='sr-only'> (the error is on this line)</span>\n              )}\n            </span>\n          </div>\n        );\n      })}\n    </div>\n  );\n}\n\nexport type QueryResultProps = {\n  run: QueryRun;\n  source: string;\n  /** How many rows are on screen while they arrive. All of them by default. */\n  shownRows?: number;\n  tab?: QueryTab;\n  defaultTab?: QueryTab;\n  onTabChange?: (tab: QueryTab) => void;\n  /** Shows \"Copied\" on Copy query for a demonstration that presses it, without touching the clipboard. */\n  isQueryCopied?: boolean;\n  /** Shows Copy query held down, for a demonstration that presses it. */\n  isCopyPressed?: boolean;\n  className?: string;\n};\n\nexport function QueryResult({\n  run,\n  source,\n  shownRows,\n  tab,\n  defaultTab = 'result',\n  onTabChange,\n  isQueryCopied = false,\n  isCopyPressed = false,\n  className,\n}: QueryResultProps) {\n  const isRunning = run.status === 'running';\n  const isDone = run.status === 'done';\n  const error = run.error;\n  const hasError = run.status === 'error' && error !== undefined;\n  const badge = STATUS_BADGE[run.status];\n  const visibleRows = run.rows.slice(0, shownRows ?? run.rows.length);\n  const csv = csvOf(run.columns, run.rows);\n\n  function changeTab(value: unknown) {\n    if (isQueryTab(value)) {\n      onTabChange?.(value);\n    }\n  }\n\n  function showErrorLine() {\n    onTabChange?.('query');\n  }\n\n  return (\n    <section\n      aria-label='Query result'\n      className={cn(\n        'bg-card hairline-ring ring-border @container relative flex flex-col gap-3 overflow-hidden rounded-xl p-4 shadow-[0_1px_2px_rgb(0_0_0/0.04)]',\n        className\n      )}\n    >\n      <RunLine isRunning={isRunning} />\n      <header className='flex items-center gap-3'>\n        <BrandTile brand='postgresql' size='lg' />\n        <div className='flex min-w-0 flex-1 flex-col'>\n          <h3 className='truncate text-sm font-medium'>{run.title}</h3>\n          <p className='text-muted-foreground truncate text-xs'>{source}</p>\n        </div>\n        <Swap swapKey={run.status}>\n          <Badge variant={badge.variant}>{badge.label}</Badge>\n        </Swap>\n      </header>\n      <Tabs\n        value={tab}\n        defaultValue={defaultTab}\n        onValueChange={changeTab}\n        className='gap-3'\n      >\n        <div className='flex flex-wrap items-center justify-between gap-2'>\n          <TabsList variant='segmented' aria-label='Query view'>\n            <TabsTab value='result'>Result</TabsTab>\n            <TabsTab value='query'>Query</TabsTab>\n          </TabsList>\n          <div className='flex flex-wrap items-center gap-2'>\n            {isDone && <CopyLabelButton text={csv} label='Copy as CSV' />}\n            <CopyLabelButton\n              text={run.sql}\n              label='Copy query'\n              isCopied={isQueryCopied}\n              isPressed={isCopyPressed}\n            />\n          </div>\n        </div>\n        <TabsPanel value='result' className='min-h-[16rem]'>\n          <div className='flex flex-col gap-2'>\n            {isRunning && (\n              <p className='text-muted-foreground flex items-center gap-2 px-1 text-xs'>\n                <LoaderIcon className='size-3.5 animate-spin' />\n                Running the query\n              </p>\n            )}\n            {hasError && (\n              <ErrorNotice error={error} onShowLine={showErrorLine} />\n            )}\n            {isDone && (\n              <>\n                <ResultTable columns={run.columns} rows={visibleRows} />\n                <ResultFooter run={run} />\n              </>\n            )}\n          </div>\n        </TabsPanel>\n        <TabsPanel value='query' className='min-h-[16rem]'>\n          <QueryCode sql={run.sql} errorLine={error?.line} />\n        </TabsPanel>\n      </Tabs>\n    </section>\n  );\n}\n\nconst SOURCE = 'Postgres, analytics database. Illustrative data';\n\nconst COLUMNS = ['region', 'orders', 'revenue_eur'];\n\nconst ROWS: QueryCell[][] = [\n  ['Kinshasa', 412, 184290],\n  ['Lubumbashi', 268, 121640],\n  ['Nairobi', 233, 98870],\n  ['Brussels', 190, 84320],\n  ['Goma', 141, 62115],\n  ['Lagos', 118, 51480],\n];\n\nconst REVENUE_SQL = [\n  'SELECT region,',\n  '       COUNT(*) AS orders,',\n  '       SUM(total_eur) AS revenue_eur',\n  'FROM orders',\n  \"WHERE placed_at >= '2026-07-01'\",\n  \"  AND placed_at < '2026-10-01'\",\n  'GROUP BY region',\n  'ORDER BY revenue_eur DESC;',\n].join('\\n');\n\nconst BROKEN_SQL = [\n  'SELECT region,',\n  '       SUM(total_eur) AS revenue_eur',\n  'FORM orders',\n  \"WHERE placed_at >= '2026-07-01'\",\n  'GROUP BY region;',\n].join('\\n');\n\nconst RUNNING_REVENUE: QueryRun = {\n  title: 'Revenue by region, Q3',\n  sql: REVENUE_SQL,\n  status: 'running',\n  columns: COLUMNS,\n  rows: [],\n};\n\nconst DONE_REVENUE: QueryRun = {\n  ...RUNNING_REVENUE,\n  status: 'done',\n  rows: ROWS,\n  ms: 38,\n};\n\nconst RUNNING_BROKEN: QueryRun = {\n  title: 'Revenue by region, second try',\n  sql: BROKEN_SQL,\n  status: 'running',\n  columns: COLUMNS,\n  rows: [],\n};\n\nconst FAILED_BROKEN: QueryRun = {\n  ...RUNNING_BROKEN,\n  status: 'error',\n  error: { message: 'syntax error at or near \"FORM\"', line: 3 },\n};\n\nconst ROW_MS = 130;\n\nconst COPIED_MS = 1500;\n\ntype QueryState = {\n  run: QueryRun;\n  shownRows: number;\n  tab: QueryTab;\n  isQueryCopied: boolean;\n};\n\ntype QueryAction =\n  | { type: 'finish' }\n  | { type: 'row' }\n  | { type: 'tab'; tab: QueryTab }\n  | { type: 'copied'; isCopied: boolean }\n  | { type: 'retry' }\n  | { type: 'fail' }\n  | { type: 'hold' };\n\ntype QueryStep = SessionStep<QueryState, QueryAction>;\n\nconst INITIAL: QueryState = {\n  run: RUNNING_REVENUE,\n  shownRows: 0,\n  tab: 'result',\n  isQueryCopied: false,\n};\n\nconst RESTING: QueryState = {\n  run: DONE_REVENUE,\n  shownRows: ROWS.length,\n  tab: 'result',\n  isQueryCopied: false,\n};\n\nfunction reducer(state: QueryState, action: QueryAction): QueryState {\n  switch (action.type) {\n    case 'finish':\n      return { ...state, run: DONE_REVENUE };\n    case 'row':\n      return { ...state, shownRows: state.shownRows + 1 };\n    case 'tab':\n      return { ...state, tab: action.tab };\n    case 'copied':\n      return { ...state, isQueryCopied: action.isCopied };\n    case 'retry':\n      return { ...state, run: RUNNING_BROKEN, shownRows: 0 };\n    case 'fail':\n      return { ...state, run: FAILED_BROKEN };\n    case 'hold':\n      return state;\n  }\n}\n\nfunction rowSteps(): QueryStep[] {\n  const steps: QueryStep[] = [];\n  for (let index = 0; index < ROWS.length; index += 1) {\n    steps.push({ after: ROW_MS, action: { type: 'row' } });\n  }\n  return steps;\n}\n\nconst SCRIPT: QueryStep[] = [\n  { after: 1500, action: { type: 'finish' } },\n  ...rowSteps(),\n  { after: 1800, by: 'demo', action: { type: 'tab', tab: 'query' } },\n  {\n    after: 1500,\n    by: 'demo',\n    press: 'copy',\n    action: { type: 'copied', isCopied: true },\n  },\n  { after: COPIED_MS, action: { type: 'copied', isCopied: false } },\n  { after: 900, by: 'demo', action: { type: 'tab', tab: 'result' } },\n  { after: 1200, action: { type: 'retry' } },\n  { after: 1100, action: { type: 'fail' } },\n  { after: 1500, by: 'demo', action: { type: 'tab', tab: 'query' } },\n  { after: 3800, action: { type: 'hold' } },\n];\n\nconst SESSION = {\n  reducer,\n  initial: INITIAL,\n  resting: RESTING,\n  script: SCRIPT,\n  loopAfter: 3000,\n};\n\nfunction announcementOf(run: QueryRun) {\n  if (run.status === 'running') {\n    return 'Running the query';\n  }\n  if (run.status === 'error') {\n    const line = run.error?.line ?? 0;\n    return `The query failed on line ${line}`;\n  }\n  const rows = countOf(run.rows.length, 'row');\n  return `The query returned ${rows} in ${run.ms ?? 0} milliseconds`;\n}\n\nexport default function Answer08() {\n  const {\n    ref,\n    isInView,\n    isAutoplaying,\n    state,\n    act,\n    pressed,\n    isFinished,\n    restart,\n  } = useSession(SESSION);\n  const canReplay = isFinished && !isAutoplaying;\n  const announcement = announcementOf(state.run);\n\n  function changeTab(tab: QueryTab) {\n    act({ type: 'tab', tab });\n  }\n\n  useKeptHeight(ref);\n\n  return (\n    <div\n      ref={ref}\n      className='flex min-h-[32rem] w-full max-w-[40rem] flex-col gap-2'\n    >\n      <ClockGate isActive={isInView}>\n        <QueryResult\n          run={state.run}\n          source={SOURCE}\n          shownRows={state.shownRows}\n          tab={state.tab}\n          onTabChange={changeTab}\n          isQueryCopied={state.isQueryCopied}\n          isCopyPressed={pressed === 'copy'}\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/answer/answer-08.tsx"
    },
    {
      "path": "registry/ndeko/components/answer/_parts/copy-label-button.tsx",
      "content": "'use client';\n\nimport { useState } from 'react';\n\nimport { useTimeouts } from '@/registry/ndeko/hooks/use-timeouts';\nimport { pressedQuietOf } from '@/registry/ndeko/lib/demonstration';\nimport { CheckIcon, CopyIcon } from '@/registry/ndeko/lib/icons';\nimport { cn } from '@/registry/ndeko/lib/utils';\nimport { Button } from '@/registry/ndeko/ui/button';\nimport { Swap } from '@/registry/ndeko/ui/chat-motion';\n\nconst COPIED_FOR_MS = 1500;\n\nconst COPY_GLYPH = <CopyIcon />;\n\nconst COPIED_GLYPH = <CheckIcon />;\n\nfunction labelOf(isCopied: boolean, label: string) {\n  if (isCopied) {\n    return 'Copied';\n  }\n  return label;\n}\n\nfunction glyphOf(isCopied: boolean) {\n  if (isCopied) {\n    return COPIED_GLYPH;\n  }\n  return COPY_GLYPH;\n}\n\ntype CopyLabelButtonProps = {\n  text: string;\n  /** What the button copies, as an action: \"Copy query\". */\n  label: string;\n  /** Shows \"Copied\" for a demonstration that presses the button, without touching the clipboard. */\n  isCopied?: boolean;\n  /** Shows the button held down, for a demonstration that presses it. */\n  isPressed?: boolean;\n  className?: string;\n};\n\nexport function CopyLabelButton({\n  text,\n  label,\n  isCopied = false,\n  isPressed = false,\n  className,\n}: CopyLabelButtonProps) {\n  const [ownCopied, setOwnCopied] = useState(false);\n  const { schedule } = useTimeouts();\n  const shownCopied = ownCopied || isCopied;\n  const shownLabel = labelOf(shownCopied, label);\n\n  async function copy() {\n    try {\n      await navigator.clipboard.writeText(text);\n    } catch {\n      return;\n    }\n    setOwnCopied(true);\n    schedule(() => setOwnCopied(false), COPIED_FOR_MS);\n  }\n\n  return (\n    <Button\n      type='button'\n      variant='outline'\n      size='sm'\n      onClick={copy}\n      className={cn(pressedQuietOf(isPressed), className)}\n    >\n      <Swap swapKey={String(shownCopied)}>{glyphOf(shownCopied)}</Swap>\n      <Swap swapKey={shownLabel}>{shownLabel}</Swap>\n    </Button>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/answer/_parts/copy-label-button.tsx"
    },
    {
      "path": "registry/ndeko/components/answer/_parts/sql.ts",
      "content": "export type SqlTokenKind =\n  'keyword' | 'function' | 'string' | 'number' | 'comment' | 'plain';\n\nexport type SqlToken = {\n  text: string;\n  kind: SqlTokenKind;\n};\n\nconst KEYWORDS = new Set([\n  'SELECT',\n  'FROM',\n  'WHERE',\n  'GROUP',\n  'BY',\n  'ORDER',\n  'HAVING',\n  'LIMIT',\n  'AS',\n  'AND',\n  'OR',\n  'NOT',\n  'IN',\n  'IS',\n  'NULL',\n  'JOIN',\n  'LEFT',\n  'INNER',\n  'ON',\n  'DISTINCT',\n  'DESC',\n  'ASC',\n  'BETWEEN',\n  'LIKE',\n  'WITH',\n]);\n\n/** One group per kind of token: comment, string, number, word, space, any other character. */\nconst TOKEN =\n  /(--.*$)|('(?:[^']|'')*')|(\\b\\d+(?:\\.\\d+)?\\b)|([A-Za-z_]\\w*)|(\\s+)|(.)/g;\n\nfunction wordKindOf(word: string, nextCharacter: string | undefined) {\n  if (KEYWORDS.has(word.toUpperCase())) {\n    return 'keyword';\n  }\n  if (nextCharacter === '(') {\n    return 'function';\n  }\n  return 'plain';\n}\n\nfunction kindOf(match: RegExpMatchArray, line: string): SqlTokenKind {\n  const [text, comment, quoted, number, word] = match;\n  if (comment) {\n    return 'comment';\n  }\n  if (quoted) {\n    return 'string';\n  }\n  if (number) {\n    return 'number';\n  }\n  if (word) {\n    const end = (match.index ?? 0) + text.length;\n    return wordKindOf(word, line[end]);\n  }\n  return 'plain';\n}\n\n/** One line of SQL cut into pieces to colour. Joining their text gives the line back. */\nexport function sqlTokensOf(line: string) {\n  const tokens: SqlToken[] = [];\n  for (const match of line.matchAll(TOKEN)) {\n    const kind = kindOf(match, line);\n    tokens.push({ text: match[0], kind });\n  }\n  return tokens;\n}\n",
      "type": "registry:component",
      "target": "components/answer/_parts/sql.ts"
    }
  ],
  "categories": [
    "components",
    "answer"
  ],
  "type": "registry:component"
}