{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "vital-signs",
  "title": "Vital signs",
  "description": "Heart rate, blood pressure, SpO₂ and temperature, each flagged against a configurable normal range.",
  "dependencies": [
    "cn@^0.5.0"
  ],
  "registryDependencies": [
    "card"
  ],
  "files": [
    {
      "path": "registry/base-nova/vital-signs/vital-signs.tsx",
      "content": "import * as React from \"react\"\nimport { cn } from \"cn\"\n\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card\"\n\n/** Inclusive bounds of the normal range. */\nexport type VitalRange = { low: number; high: number }\nexport type VitalRanges = {\n  /** Beats per minute. */\n  heartRate: VitalRange\n  /** mmHg. */\n  systolic: VitalRange\n  /** mmHg. */\n  diastolic: VitalRange\n  /** Percent. */\n  spo2: VitalRange\n  /** In the panel's temperature unit. */\n  temperature: VitalRange\n  /** Breaths per minute. */\n  respiratoryRate: VitalRange\n}\nexport type VitalReadings = {\n  heartRate?: number | null\n  systolic?: number | null\n  diastolic?: number | null\n  spo2?: number | null\n  temperature?: number | null\n  respiratoryRate?: number | null\n}\nexport type VitalSign =\n  \"heartRate\" | \"bloodPressure\" | \"spo2\" | \"temperature\" | \"respiratoryRate\"\nexport type VitalStatus = \"low\" | \"normal\" | \"high\" | \"missing\"\nexport type TemperatureUnit = \"celsius\" | \"fahrenheit\"\nexport type VitalSignsProps = {\n  readings: VitalReadings\n  /** Merged over `adultVitalRanges`, one vital at a time. */\n  ranges?: Partial<VitalRanges>\n  /** Which vitals to show, in order. */\n  vitals?: readonly VitalSign[]\n  /** The unit of `readings.temperature` and `ranges.temperature`. */\n  temperatureUnit?: TemperatureUnit\n  /** Used to format numbers. */\n  locale?: string\n  title?: string\n  /** Shown under the title, such as when the readings were taken. */\n  description?: React.ReactNode\n  className?: string\n}\n\n/**\n * Common resting adult ranges, as a starting point rather than clinical\n * guidance. Temperature is in degrees Celsius.\n */\nexport const adultVitalRanges: VitalRanges = {\n  heartRate: { low: 60, high: 100 },\n  systolic: { low: 90, high: 120 },\n  diastolic: { low: 60, high: 80 },\n  spo2: { low: 95, high: 100 },\n  temperature: { low: 36.1, high: 37.2 },\n  respiratoryRate: { low: 12, high: 20 },\n}\nexport const defaultVitals: readonly VitalSign[] = [\n  \"heartRate\",\n  \"bloodPressure\",\n  \"spo2\",\n  \"temperature\",\n]\n\nexport function vitalStatus(\n  value: number | null | undefined,\n  range: VitalRange\n): VitalStatus {\n  if (value == null || !Number.isFinite(value)) return \"missing\"\n  if (value < range.low) return \"low\"\n  if (value > range.high) return \"high\"\n  return \"normal\"\n}\n\n/** High wins over low, so a reading that is both reads as high. */\nexport function bloodPressureStatus(\n  systolic: VitalStatus,\n  diastolic: VitalStatus\n): VitalStatus {\n  if (systolic === \"missing\" || diastolic === \"missing\") return \"missing\"\n  if (systolic === \"high\" || diastolic === \"high\") return \"high\"\n  if (systolic === \"low\" || diastolic === \"low\") return \"low\"\n  return \"normal\"\n}\n\nfunction celsiusToFahrenheit({ low, high }: VitalRange): VitalRange {\n  const convert = (value: number) =>\n    Math.round(((value * 9) / 5 + 32) * 10) / 10\n  return { low: convert(low), high: convert(high) }\n}\n\n/**\n * Resolves the ranges a panel uses: defaults converted to the temperature\n * unit, then any overrides as given.\n */\nexport function resolveVitalRanges(\n  ranges: Partial<VitalRanges> = {},\n  temperatureUnit: TemperatureUnit = \"celsius\"\n): VitalRanges {\n  const defaults =\n    temperatureUnit === \"fahrenheit\"\n      ? {\n          ...adultVitalRanges,\n          temperature: celsiusToFahrenheit(adultVitalRanges.temperature),\n        }\n      : adultVitalRanges\n  return { ...defaults, ...ranges }\n}\n\nexport type VitalAssessment = {\n  vital: VitalSign\n  label: string\n  unit: string\n  /** Formatted value, or null without a reading. */\n  value: string | null\n  /** Formatted normal range. */\n  range: string\n  status: VitalStatus\n  /** Status as text, which never relies on colour. */\n  statusLabel: string\n}\n\nconst statusLabels: Record<VitalStatus, string> = {\n  low: \"Low\",\n  normal: \"Normal\",\n  high: \"High\",\n  missing: \"No reading\",\n}\n\nexport function assessVital(\n  vital: VitalSign,\n  readings: VitalReadings,\n  ranges: VitalRanges,\n  temperatureUnit: TemperatureUnit = \"celsius\",\n  locale = \"en-US\"\n): VitalAssessment {\n  const format = (value: number, digits = 0) =>\n    value.toLocaleString(locale, {\n      minimumFractionDigits: digits,\n      maximumFractionDigits: digits,\n    })\n  const span = ({ low, high }: VitalRange, digits = 0) =>\n    `${format(low, digits)}–${format(high, digits)}`\n  if (vital === \"bloodPressure\") {\n    const systolic = vitalStatus(readings.systolic, ranges.systolic)\n    const diastolic = vitalStatus(readings.diastolic, ranges.diastolic)\n    const status = bloodPressureStatus(systolic, diastolic)\n    // Name the out-of-range part when only one is, or when they disagree.\n    const parts = [\n      systolic !== \"normal\" ? `${systolic} systolic` : null,\n      diastolic !== \"normal\" ? `${diastolic} diastolic` : null,\n    ].filter((part) => part !== null)\n    const detail =\n      status === \"missing\" || status === \"normal\" || systolic === diastolic\n        ? statusLabels[status]\n        : parts.join(\", \").replace(/^./, (letter) => letter.toUpperCase())\n    return {\n      vital,\n      label: \"Blood pressure\",\n      unit: \"mmHg\",\n      value:\n        status === \"missing\"\n          ? null\n          : `${format(readings.systolic!)}/${format(readings.diastolic!)}`,\n      range: `${span(ranges.systolic)} / ${span(ranges.diastolic)}`,\n      status,\n      statusLabel: detail,\n    }\n  }\n  const meta = {\n    heartRate: { label: \"Heart rate\", unit: \"bpm\", digits: 0 },\n    spo2: { label: \"SpO₂\", unit: \"%\", digits: 0 },\n    temperature: {\n      label: \"Temperature\",\n      unit: temperatureUnit === \"fahrenheit\" ? \"°F\" : \"°C\",\n      digits: 1,\n    },\n    respiratoryRate: { label: \"Respiratory rate\", unit: \"/min\", digits: 0 },\n  }[vital]\n  const value = readings[vital]\n  const status = vitalStatus(value, ranges[vital])\n  return {\n    vital,\n    label: meta.label,\n    unit: meta.unit,\n    value: status === \"missing\" ? null : format(value!, meta.digits),\n    range: span(ranges[vital], meta.digits),\n    status,\n    statusLabel: statusLabels[status],\n  }\n}\n\n// Inline icons keep the item free of an icon library. Each status has its own\n// shape, so it reads without colour as well as with the text beside it.\nfunction StatusIcon({\n  status,\n  className,\n}: {\n  status: VitalStatus\n  className?: string\n}) {\n  const paths = {\n    normal: <path d=\"M20 6 9 17l-5-5\" />,\n    high: <path d=\"m5 12 7-7 7 7M12 19V5\" />,\n    low: <path d=\"M12 5v14m7-7-7 7-7-7\" />,\n    missing: <path d=\"M5 12h14\" />,\n  }\n  return (\n    <svg\n      aria-hidden=\"true\"\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth=\"2.5\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      className={cn(\"size-3.5 shrink-0\", className)}\n    >\n      {paths[status]}\n    </svg>\n  )\n}\n\nexport function VitalSigns({\n  readings,\n  ranges,\n  vitals = defaultVitals,\n  temperatureUnit = \"celsius\",\n  locale = \"en-US\",\n  title = \"Vital signs\",\n  description,\n  className,\n}: VitalSignsProps) {\n  const resolved = resolveVitalRanges(ranges, temperatureUnit)\n  return (\n    <Card role=\"region\" aria-label={title} className={cn(\"w-full\", className)}>\n      <CardHeader>\n        <CardTitle>{title}</CardTitle>\n        {description != null && (\n          <CardDescription>{description}</CardDescription>\n        )}\n      </CardHeader>\n      <CardContent>\n        <dl className=\"grid grid-cols-[repeat(auto-fit,minmax(9.5rem,1fr))] gap-3\">\n          {vitals.map((vital) => {\n            const item = assessVital(\n              vital,\n              readings,\n              resolved,\n              temperatureUnit,\n              locale\n            )\n            const abnormal = item.status === \"low\" || item.status === \"high\"\n            return (\n              <div\n                key={vital}\n                data-vital={vital}\n                data-status={item.status}\n                className={cn(\n                  \"flex min-w-0 flex-col gap-1.5 rounded-lg border p-3\",\n                  abnormal && \"border-destructive\"\n                )}\n              >\n                <dt className=\"text-xs font-medium text-muted-foreground\">\n                  {item.label}\n                </dt>\n                <dd className=\"flex items-baseline gap-1\">\n                  {item.value === null ? (\n                    <span\n                      aria-hidden=\"true\"\n                      className=\"text-2xl font-semibold text-muted-foreground\"\n                    >\n                      —\n                    </span>\n                  ) : (\n                    <>\n                      <span className=\"text-2xl font-semibold tabular-nums\">\n                        {item.value}\n                      </span>\n                      <span className=\"text-xs text-muted-foreground\">\n                        {item.unit}\n                      </span>\n                    </>\n                  )}\n                </dd>\n                <dd\n                  className={cn(\n                    \"flex items-center gap-1 text-xs\",\n                    abnormal\n                      ? \"font-semibold text-foreground\"\n                      : \"font-medium text-muted-foreground\"\n                  )}\n                >\n                  <StatusIcon\n                    status={item.status}\n                    className={abnormal ? \"text-destructive\" : undefined}\n                  />\n                  {item.statusLabel}\n                </dd>\n                <dd className=\"text-xs text-muted-foreground tabular-nums\">\n                  Normal {item.range} {item.unit}\n                </dd>\n              </div>\n            )\n          })}\n        </dl>\n      </CardContent>\n    </Card>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "docs": "Vital signs docs: https://organui.com/docs/components/vital-signs",
  "categories": [
    "clinical"
  ],
  "type": "registry:ui"
}