{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "lab-results",
  "title": "Lab results table",
  "description": "Lab values flagged against the reference ranges you pass, with text and icon flags and a trend sparkline.",
  "dependencies": [
    "cn@^0.5.0"
  ],
  "registryDependencies": [
    "table"
  ],
  "files": [
    {
      "path": "registry/base-nova/lab-results/lab-results.tsx",
      "content": "import * as React from \"react\"\nimport { cn } from \"cn\"\n\nimport {\n  Table,\n  TableBody,\n  TableCaption,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from \"@/components/ui/table\"\n\nexport type LabFlag =\n  | \"critical-low\"\n  | \"low\"\n  | \"normal\"\n  | \"high\"\n  | \"critical-high\"\n  | \"abnormal\"\n  | \"unflagged\"\n\nexport type LabReferenceRange = {\n  /** Lowest normal value, inclusive. */\n  low?: number\n  /** Highest normal value, inclusive. */\n  high?: number\n  /** Values below this are flagged critical low. */\n  criticalLow?: number\n  /** Values above this are flagged critical high. */\n  criticalHigh?: number\n  /** Shown instead of the generated range, such as \"Negative\". */\n  text?: string\n}\n\nexport type LabMeasurement = {\n  value: number\n  /** An ISO 8601 date or date-time. */\n  date: string\n}\n\nexport type LabResult = {\n  id: string\n  name: string\n  /** A number is flagged against the range; text, such as \"Negative\", is not. */\n  value: number | string\n  unit?: string\n  /** An ISO 8601 date or date-time. */\n  date?: string\n  referenceRange?: LabReferenceRange\n  /** The flag reported by the laboratory. Overrides the computed flag. */\n  flag?: LabFlag\n  /** Earlier and current results for the trend sparkline, in any order. */\n  history?: readonly LabMeasurement[]\n  /** Fixed decimal places for the value, range and trend. */\n  decimals?: number\n}\n\nexport type LabResultsTableProps = {\n  results: readonly LabResult[]\n  /** Names the table for assistive technology and shows below it. */\n  caption?: React.ReactNode\n  /** Defaults to a UTC date such as \"Oct 1, 2026\". */\n  formatDate?: (date: string) => string\n  /** Shows the trend column. Defaults to true. */\n  showTrend?: boolean\n  className?: string\n}\n\n/** Flags a numeric value against its reference range. */\nexport function labFlag(\n  value: number | string,\n  range: LabReferenceRange = {}\n): LabFlag {\n  if (typeof value !== \"number\" || Number.isNaN(value)) return \"unflagged\"\n  const { low, high, criticalLow, criticalHigh } = range\n  if (criticalLow !== undefined && value < criticalLow) return \"critical-low\"\n  if (criticalHigh !== undefined && value > criticalHigh) return \"critical-high\"\n  if (low !== undefined && value < low) return \"low\"\n  if (high !== undefined && value > high) return \"high\"\n  if (low === undefined && high === undefined) return \"unflagged\"\n  return \"normal\"\n}\n\nexport function resultFlag(result: LabResult): LabFlag {\n  return result.flag ?? labFlag(result.value, result.referenceRange)\n}\n\nexport const labFlagLabels: Record<LabFlag, string> = {\n  \"critical-low\": \"Critical low\",\n  low: \"Low\",\n  normal: \"Normal\",\n  high: \"High\",\n  \"critical-high\": \"Critical high\",\n  abnormal: \"Abnormal\",\n  unflagged: \"Not flagged\",\n}\n\nfunction formatNumber(value: number, decimals?: number) {\n  return decimals === undefined ? String(value) : value.toFixed(decimals)\n}\n\nfunction formatValue(value: number | string, decimals?: number) {\n  return typeof value === \"number\" ? formatNumber(value, decimals) : value\n}\n\n/** The range as printed on a report, such as \"70–99\" or \"≤ 200\". */\nexport function formatReferenceRange(\n  range: LabReferenceRange | undefined,\n  decimals?: number\n): string | null {\n  if (!range) return null\n  if (range.text) return range.text\n  const { low, high } = range\n  if (low !== undefined && high !== undefined)\n    return `${formatNumber(low, decimals)}–${formatNumber(high, decimals)}`\n  if (high !== undefined) return `≤ ${formatNumber(high, decimals)}`\n  if (low !== undefined) return `≥ ${formatNumber(low, decimals)}`\n  return null\n}\n\nconst dateFormat = new Intl.DateTimeFormat(\"en-US\", {\n  dateStyle: \"medium\",\n  timeZone: \"UTC\",\n})\n\n// A fixed locale and time zone render the same on the server and the client.\nfunction defaultFormatDate(date: string) {\n  const time = Date.parse(date)\n  return Number.isNaN(time) ? date : dateFormat.format(time)\n}\n\nfunction sortedHistory(history: readonly LabMeasurement[]) {\n  return [...history].sort((a, b) => Date.parse(a.date) - Date.parse(b.date))\n}\n\n/** A sentence describing the trend, used as the sparkline's accessible name. */\nexport function describeTrend(\n  result: LabResult,\n  formatDate: (date: string) => string = defaultFormatDate\n): string | null {\n  if (!result.history || result.history.length < 2) return null\n  const points = sortedHistory(result.history)\n  const first = points[0]!\n  const last = points.at(-1)!\n  const unit = result.unit ? ` ${result.unit}` : \"\"\n  const direction =\n    last.value > first.value\n      ? \"rising\"\n      : last.value < first.value\n        ? \"falling\"\n        : \"unchanged\"\n  return `${result.name} trend, ${points.length} results from ${formatDate(\n    first.date\n  )} to ${formatDate(last.date)}: ${formatNumber(\n    first.value,\n    result.decimals\n  )} to ${formatNumber(last.value, result.decimals)}${unit}, ${direction}`\n}\n\nfunction FlagIcon({ flag }: { flag: LabFlag }) {\n  const common = {\n    \"aria-hidden\": true,\n    viewBox: \"0 0 16 16\",\n    className: \"size-4 shrink-0\",\n    fill: \"none\",\n    stroke: \"currentColor\",\n    strokeWidth: 1.75,\n    strokeLinecap: \"round\" as const,\n    strokeLinejoin: \"round\" as const,\n  }\n  switch (flag) {\n    case \"high\":\n      return (\n        <svg {...common}>\n          <path d=\"M8 13V3M4 7l4-4 4 4\" />\n        </svg>\n      )\n    case \"low\":\n      return (\n        <svg {...common}>\n          <path d=\"M8 3v10M4 9l4 4 4-4\" />\n        </svg>\n      )\n    case \"critical-high\":\n    case \"critical-low\":\n    case \"abnormal\":\n      return (\n        <svg {...common}>\n          <path d=\"M8 1.75 15 14H1z\" />\n          <path d=\"M8 6v3.5M8 11.75v.01\" />\n        </svg>\n      )\n    case \"normal\":\n      return (\n        <svg {...common}>\n          <path d=\"m3 8.5 3 3 7-7\" />\n        </svg>\n      )\n    default:\n      return (\n        <svg {...common}>\n          <path d=\"M4 8h8\" />\n        </svg>\n      )\n  }\n}\n\nfunction isAbnormal(flag: LabFlag) {\n  return flag !== \"normal\" && flag !== \"unflagged\"\n}\n\nfunction isCritical(flag: LabFlag) {\n  return flag === \"critical-low\" || flag === \"critical-high\"\n}\n\nexport function LabFlagLabel({\n  flag,\n  className,\n}: {\n  flag: LabFlag\n  className?: string\n}) {\n  return (\n    <span\n      data-slot=\"lab-flag\"\n      data-flag={flag}\n      className={cn(\n        \"inline-flex items-center gap-1 text-xs font-medium\",\n        isCritical(flag)\n          ? \"text-destructive\"\n          : isAbnormal(flag)\n            ? \"text-foreground\"\n            : \"text-muted-foreground\",\n        className\n      )}\n    >\n      <FlagIcon flag={flag} />\n      {labFlagLabels[flag]}\n    </span>\n  )\n}\n\nconst sparkWidth = 96\nconst sparkHeight = 28\nconst sparkPad = 3\n\nexport function LabSparkline({\n  result,\n  label,\n  className,\n}: {\n  result: LabResult\n  label: string\n  className?: string\n}) {\n  const points = sortedHistory(result.history ?? [])\n  const range = result.referenceRange ?? {}\n  const values = points.map(({ value }) => value)\n  const bounds = [\n    ...values,\n    ...[range.low, range.high].filter(\n      (value): value is number => value !== undefined\n    ),\n  ]\n  let min = Math.min(...bounds)\n  let max = Math.max(...bounds)\n  if (min === max) {\n    min -= 1\n    max += 1\n  }\n  const y = (value: number) =>\n    sparkPad + ((max - value) / (max - min)) * (sparkHeight - 2 * sparkPad)\n  const x = (index: number) =>\n    sparkPad +\n    (points.length > 1\n      ? (index / (points.length - 1)) * (sparkWidth - 2 * sparkPad)\n      : (sparkWidth - 2 * sparkPad) / 2)\n  const bandTop = range.high !== undefined ? y(range.high) : 0\n  const bandBottom = range.low !== undefined ? y(range.low) : sparkHeight\n  const hasBand = range.low !== undefined || range.high !== undefined\n  return (\n    <svg\n      role=\"img\"\n      aria-label={label}\n      data-slot=\"lab-sparkline\"\n      viewBox={`0 0 ${sparkWidth} ${sparkHeight}`}\n      width={sparkWidth}\n      height={sparkHeight}\n      className={cn(\n        \"h-auto w-12 overflow-visible text-muted-foreground @xs:w-16 @sm:w-24\",\n        className\n      )}\n    >\n      {hasBand && (\n        <rect\n          data-slot=\"lab-sparkline-range\"\n          x={0}\n          y={bandTop}\n          width={sparkWidth}\n          height={Math.max(bandBottom - bandTop, 0)}\n          className=\"fill-muted\"\n        />\n      )}\n      <polyline\n        points={points.map((point, i) => `${x(i)},${y(point.value)}`).join(\" \")}\n        fill=\"none\"\n        stroke=\"currentColor\"\n        strokeWidth={1.5}\n        strokeLinejoin=\"round\"\n        strokeLinecap=\"round\"\n      />\n      {points.map((point, i) => {\n        const outside = isAbnormal(labFlag(point.value, range))\n        const last = i === points.length - 1\n        if (!outside && !last) return null\n        return (\n          <circle\n            key={`${point.date}-${i}`}\n            cx={x(i)}\n            cy={y(point.value)}\n            r={last ? 2.75 : 2}\n            className={cn(\n              outside ? \"fill-background stroke-foreground\" : \"fill-foreground\",\n              last && outside && \"fill-foreground\"\n            )}\n            strokeWidth={outside ? 1.5 : 0}\n          />\n        )\n      })}\n    </svg>\n  )\n}\n\nexport function LabResultsTable({\n  results,\n  caption,\n  formatDate = defaultFormatDate,\n  showTrend = true,\n  className,\n}: LabResultsTableProps) {\n  return (\n    <div\n      data-slot=\"lab-results\"\n      className={cn(\"@container w-full min-w-0\", className)}\n    >\n      <Table>\n        {caption && <TableCaption>{caption}</TableCaption>}\n        <TableHeader>\n          <TableRow>\n            <TableHead scope=\"col\">Test</TableHead>\n            <TableHead scope=\"col\">Result</TableHead>\n            <TableHead scope=\"col\" className=\"hidden @lg:table-cell\">\n              Reference range\n            </TableHead>\n            {showTrend && <TableHead scope=\"col\">Trend</TableHead>}\n          </TableRow>\n        </TableHeader>\n        <TableBody>\n          {results.map((result) => {\n            const flag = resultFlag(result)\n            const unit = result.unit ? ` ${result.unit}` : \"\"\n            const range = formatReferenceRange(\n              result.referenceRange,\n              result.decimals\n            )\n            const rangeText = range\n              ? `${range}${result.referenceRange?.text ? \"\" : unit}`\n              : \"Not given\"\n            const trend = showTrend ? describeTrend(result, formatDate) : null\n            return (\n              <TableRow key={result.id} data-flag={flag}>\n                <TableHead\n                  scope=\"row\"\n                  className=\"h-auto py-2 font-normal whitespace-normal\"\n                >\n                  <span className=\"font-medium\">{result.name}</span>\n                  {result.date && (\n                    <span className=\"block text-xs text-muted-foreground\">\n                      {formatDate(result.date)}\n                    </span>\n                  )}\n                  <span className=\"block text-xs text-muted-foreground @lg:hidden\">\n                    Range: {rangeText}\n                  </span>\n                </TableHead>\n                <TableCell className=\"whitespace-normal\">\n                  <span\n                    className={cn(\n                      \"tabular-nums\",\n                      isAbnormal(flag) && \"font-semibold\"\n                    )}\n                  >\n                    {formatValue(result.value, result.decimals)}\n                    {unit && (\n                      <span className=\"font-normal text-muted-foreground\">\n                        {unit}\n                      </span>\n                    )}\n                  </span>\n                  <LabFlagLabel flag={flag} className=\"flex\" />\n                </TableCell>\n                <TableCell className=\"hidden text-muted-foreground tabular-nums @lg:table-cell\">\n                  {rangeText}\n                </TableCell>\n                {showTrend && (\n                  <TableCell>\n                    {trend ? (\n                      <LabSparkline result={result} label={trend} />\n                    ) : (\n                      <>\n                        <span aria-hidden className=\"text-muted-foreground\">\n                          —\n                        </span>\n                        <span className=\"sr-only\">No earlier results</span>\n                      </>\n                    )}\n                  </TableCell>\n                )}\n              </TableRow>\n            )\n          })}\n        </TableBody>\n      </Table>\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "docs": "Lab results table docs: https://organui.com/docs/components/lab-results",
  "categories": [
    "clinical"
  ],
  "type": "registry:ui"
}