{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "patient-banner",
  "title": "Patient banner",
  "description": "The patient's identity at the top of a clinical screen: name, date of birth and age, sex, identifiers, alerts, and allergies that tell not recorded from none known.",
  "dependencies": [
    "cn@^0.5.0"
  ],
  "registryDependencies": [
    "badge"
  ],
  "files": [
    {
      "path": "registry/base-nova/patient-banner/patient-banner.tsx",
      "content": "import * as React from \"react\"\nimport { cn } from \"cn\"\n\nimport { Badge } from \"@/components/ui/badge\"\nimport { IconPlaceholder } from \"@/icons/icon-placeholder\"\n\nexport type PatientName = {\n  /** The family name, shown in capitals. */\n  family: string\n  /** Given names, in order. */\n  given?: readonly string[]\n  /** A title such as \"Mrs\" or \"Dr\", shown in brackets after the name. */\n  prefix?: string\n}\n\nexport type PatientIdentifier = {\n  /** What the number is, such as \"NHS number\" or \"MRN\". */\n  label: string\n  /** The number as it should read; `formatNhsNumber` spaces an NHS number. */\n  value: string\n}\n\nexport type Patient = {\n  name: PatientName\n  /** An ISO 8601 date. */\n  birthDate: string\n  /** As recorded, such as \"Female\". */\n  sex?: string\n  /** The patient's numbers, the primary one first. */\n  identifiers?: readonly PatientIdentifier[]\n  /** An ISO 8601 date. Shown in place of the age. */\n  deceasedDate?: string\n}\n\n/**\n * Recorded allergies: each substance, `\"none-known\"` once \"no known\n * allergies\" has been recorded, or nothing (or an empty list) when they\n * have not been asked about. The banner shows the three differently,\n * because \"not recorded\" is not \"none\".\n */\nexport type PatientAllergies = readonly string[] | \"none-known\"\n\nexport type PatientBannerProps = React.ComponentProps<\"section\"> & {\n  /** Name, date of birth, sex, identifiers and any date of death. */\n  patient: Patient\n  /** Substances, `\"none-known\"`, or nothing when not recorded. */\n  allergies?: PatientAllergies\n  /** Warnings to see before anything else, such as \"DNACPR\" or \"Safeguarding\". */\n  alerts?: readonly string[]\n  /** The day age is counted on. Defaults to today. */\n  today?: Date\n  /** Defaults to an unambiguous UTC date such as \"12-Jan-1958\". */\n  formatDate?: (date: string) => string\n}\n\nconst months = [\n  \"Jan\",\n  \"Feb\",\n  \"Mar\",\n  \"Apr\",\n  \"May\",\n  \"Jun\",\n  \"Jul\",\n  \"Aug\",\n  \"Sep\",\n  \"Oct\",\n  \"Nov\",\n  \"Dec\",\n]\n\n/**\n * A date as \"12-Jan-1958\", in UTC, the form the NHS patient banner uses:\n * day, month and year can't be mistaken for one another in any locale.\n */\nexport function formatBannerDate(date: string) {\n  const time = Date.parse(date)\n  if (Number.isNaN(time)) return date\n  const day = new Date(time)\n  return `${String(day.getUTCDate()).padStart(2, \"0\")}-${\n    months[day.getUTCMonth()]\n  }-${day.getUTCFullYear()}`\n}\n\n/** \"SMITH, Jane Anne (Mrs)\": the family name first and in capitals. */\nexport function formatPatientName({ family, given = [], prefix }: PatientName) {\n  const first = given.length ? `, ${given.join(\" \")}` : \"\"\n  return `${family.toUpperCase()}${first}${prefix ? ` (${prefix})` : \"\"}`\n}\n\n/**\n * Age as a banner shows it: days under four weeks, months under two years,\n * then completed years.\n */\nexport function formatPatientAge(birthDate: string, on: Date) {\n  const birth = new Date(Date.parse(birthDate))\n  const days = Math.floor((on.getTime() - birth.getTime()) / 86_400_000)\n  if (Number.isNaN(days) || days < 0) return null\n  if (days < 28) return days === 1 ? \"1 day\" : `${days} days`\n  let months =\n    (on.getUTCFullYear() - birth.getUTCFullYear()) * 12 +\n    on.getUTCMonth() -\n    birth.getUTCMonth()\n  if (on.getUTCDate() < birth.getUTCDate()) months--\n  if (months < 24) return months === 1 ? \"1 month\" : `${months} months`\n  return `${Math.floor(months / 12)} years`\n}\n\n/** Checks an NHS number's length and modulus 11 check digit. */\nexport function isValidNhsNumber(value: string) {\n  const digits = value.replace(/\\s/g, \"\")\n  if (!/^\\d{10}$/.test(digits)) return false\n  const sum = [...digits.slice(0, 9)].reduce(\n    (total, digit, i) => total + Number(digit) * (10 - i),\n    0\n  )\n  const check = 11 - (sum % 11)\n  if (check === 10) return false\n  return (check === 11 ? 0 : check) === Number(digits[9])\n}\n\n/** Spaces an NHS number 3-3-4, as \"943 476 5919\", so it is read in groups. */\nexport function formatNhsNumber(value: string) {\n  const digits = value.replace(/\\s/g, \"\")\n  return /^\\d{10}$/.test(digits)\n    ? `${digits.slice(0, 3)} ${digits.slice(3, 6)} ${digits.slice(6)}`\n    : value\n}\n\nfunction Field({\n  label,\n  children,\n}: {\n  label: string\n  children: React.ReactNode\n}) {\n  return (\n    <div className=\"flex items-baseline gap-1.5\">\n      <dt className=\"text-muted-foreground\">{label}</dt>\n      <dd className=\"font-medium tabular-nums\">{children}</dd>\n    </div>\n  )\n}\n\nfunction Allergies({ allergies }: { allergies?: PatientAllergies }) {\n  if (allergies === undefined || allergies.length === 0)\n    return (\n      <span\n        data-slot=\"patient-allergies\"\n        data-state=\"not-recorded\"\n        className=\"inline-flex items-center gap-1.5 text-muted-foreground\"\n      >\n        <IconPlaceholder\n          lucide=\"CircleHelpIcon\"\n          tabler=\"IconHelpCircle\"\n          hugeicons=\"HelpCircleIcon\"\n          phosphor=\"QuestionIcon\"\n          remixicon=\"RiQuestionLine\"\n          aria-hidden=\"true\"\n          className=\"size-4 shrink-0\"\n        />\n        Allergies not recorded\n      </span>\n    )\n  if (allergies === \"none-known\")\n    return (\n      <span\n        data-slot=\"patient-allergies\"\n        data-state=\"none-known\"\n        className=\"inline-flex items-center gap-1.5\"\n      >\n        <IconPlaceholder\n          lucide=\"CheckIcon\"\n          tabler=\"IconCheck\"\n          hugeicons=\"Tick02Icon\"\n          phosphor=\"CheckIcon\"\n          remixicon=\"RiCheckLine\"\n          aria-hidden=\"true\"\n          className=\"size-4 shrink-0 text-muted-foreground\"\n        />\n        No known allergies\n      </span>\n    )\n  return (\n    <span\n      data-slot=\"patient-allergies\"\n      data-state=\"recorded\"\n      className=\"inline-flex items-start gap-1.5 font-semibold\"\n    >\n      <IconPlaceholder\n        lucide=\"TriangleAlertIcon\"\n        tabler=\"IconAlertTriangle\"\n        hugeicons=\"Alert02Icon\"\n        phosphor=\"WarningIcon\"\n        remixicon=\"RiErrorWarningLine\"\n        aria-hidden=\"true\"\n        className=\"mt-0.5 size-4 shrink-0\"\n      />\n      <span>\n        Allergies: <span className=\"font-normal\">{allergies.join(\", \")}</span>\n      </span>\n    </span>\n  )\n}\n\nexport function PatientBanner({\n  patient,\n  allergies,\n  alerts = [],\n  today,\n  formatDate = formatBannerDate,\n  className,\n  ...props\n}: PatientBannerProps) {\n  const name = formatPatientName(patient.name)\n  const age = patient.deceasedDate\n    ? null\n    : formatPatientAge(patient.birthDate, today ?? new Date())\n  return (\n    <section\n      data-slot=\"patient-banner\"\n      aria-label={`Patient: ${name}`}\n      className={cn(\n        \"@container flex w-full min-w-0 flex-col gap-2 rounded-lg border bg-card px-4 py-3 text-sm text-card-foreground\",\n        className\n      )}\n      {...props}\n    >\n      <div className=\"flex flex-wrap items-center gap-x-3 gap-y-1\">\n        <h2 className=\"text-base font-semibold tracking-tight\">{name}</h2>\n        {patient.deceasedDate && (\n          <Badge variant=\"secondary\">\n            Died {formatDate(patient.deceasedDate)}\n          </Badge>\n        )}\n        {alerts.map((alert) => (\n          <Badge key={alert} variant=\"outline\" data-slot=\"patient-alert\">\n            <IconPlaceholder\n              lucide=\"CircleAlertIcon\"\n              tabler=\"IconAlertCircle\"\n              hugeicons=\"AlertCircleIcon\"\n              phosphor=\"WarningCircleIcon\"\n              remixicon=\"RiErrorWarningLine\"\n              aria-hidden=\"true\"\n              data-icon=\"inline-start\"\n            />\n            {alert}\n          </Badge>\n        ))}\n      </div>\n      <dl className=\"flex flex-wrap gap-x-5 gap-y-1\">\n        <Field label=\"Born\">\n          {formatDate(patient.birthDate)}\n          {age && (\n            <span className=\"font-normal text-muted-foreground\"> ({age})</span>\n          )}\n        </Field>\n        {patient.sex && <Field label=\"Sex\">{patient.sex}</Field>}\n        {patient.identifiers?.map((identifier) => (\n          <Field key={identifier.label} label={identifier.label}>\n            {identifier.value}\n          </Field>\n        ))}\n      </dl>\n      <Allergies allergies={allergies} />\n    </section>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "docs": "Patient banner docs: https://organui.com/docs/components/patient-banner",
  "categories": [
    "clinical"
  ],
  "type": "registry:ui"
}