{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "condition-viewer",
  "title": "Condition viewer",
  "description": "Shows a condition on organ models: the structures it affects or spares at each stage and scale, with a panel for its ICD-10 code, course, findings and key investigations. Every condition component is built on it.",
  "dependencies": [
    "@base-ui/react@^1.8.0",
    "class-variance-authority@^0.7.1",
    "cn@^0.5.0"
  ],
  "registryDependencies": [
    "@organui/organ-viewer",
    "badge",
    "button"
  ],
  "files": [
    {
      "path": "registry/base-nova/condition-viewer/condition-viewer.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"cn\"\nimport { Badge } from \"@/components/ui/badge\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  conditionModel,\n  scaleOf,\n  stageOf,\n  type Condition,\n  type ConditionEvent,\n  type ConditionInvestigation,\n  type ConditionScale,\n  type ConditionStage,\n} from \"@/components/condition-model\"\nimport {\n  OrganViewer,\n  type OrganModel,\n  type OrganViewerProps,\n} from \"@/components/organ-viewer\"\nimport { IconPlaceholder } from \"@/icons/icon-placeholder\"\n\nfunction useControllable<T>(\n  value: T | undefined,\n  defaultValue: T,\n  onChange?: (value: T) => void\n) {\n  const [inner, setInner] = React.useState(defaultValue)\n  const controlled = value !== undefined\n  const set = React.useCallback(\n    (next: T) => {\n      if (!controlled) setInner(next)\n      onChange?.(next)\n    },\n    [controlled, onChange]\n  )\n  return [controlled ? value : inner, set] as const\n}\n\nexport type ConditionViewerProps = Omit<\n  OrganViewerProps,\n  \"model\" | \"selectedId\" | \"onSelectionChange\"\n> & {\n  /** The condition: its scales, stages, findings and investigations. */\n  condition: Condition\n  /** The scale shown, by ID. */\n  scale?: string\n  /** The scale shown first when `scale` is not set. Defaults to the first. */\n  defaultScale?: string\n  /** Called with the scale a reader chooses. */\n  onScaleChange?: (scale: string) => void\n  /** The stage shown, by ID. */\n  stage?: string\n  /** The stage shown first when `stage` is not set. Defaults to the first. */\n  defaultStage?: string\n  /** Called with the stage a reader chooses. */\n  onStageChange?: (stage: string) => void\n  /** The selected structure ID, or null. */\n  selectedId?: string | null\n  /** Called with the structure a reader selects, or null when they clear it. */\n  onSelectionChange?: (id: string | null) => void\n  /** Shows the panel with the scales, findings and investigations. Defaults to true. */\n  panel?: boolean\n  /** Shows the course under the model as a timeline, with its events and a play button. Defaults to true; a condition with one stage has none. */\n  timeline?: boolean\n}\n\nexport function ConditionViewer({\n  condition,\n  scale: scaleProp,\n  defaultScale = condition.scales[0]!.id,\n  onScaleChange,\n  stage: stageProp,\n  defaultStage = condition.stages?.[0]?.id ?? \"\",\n  onStageChange,\n  selectedId: selectedProp,\n  onSelectionChange,\n  panel = true,\n  timeline = true,\n  label,\n  className,\n  ...props\n}: ConditionViewerProps) {\n  const [scaleId, setScaleId] = useControllable(\n    scaleProp,\n    defaultScale,\n    onScaleChange\n  )\n  const [stageId, setStageId] = useControllable(\n    stageProp,\n    defaultStage,\n    onStageChange\n  )\n  const [selectedId, setSelectedId] = useControllable<string | null>(\n    selectedProp,\n    null,\n    onSelectionChange\n  )\n  const scale = scaleOf(condition, scaleId)\n  const stage = stageOf(condition, stageId)\n  const model = React.useMemo(\n    () => conditionModel(condition, scale.id, stage?.id),\n    [condition, scale.id, stage?.id]\n  )\n  const changeScale = (next: string) => {\n    if (next === scale.id) return\n    setScaleId(next)\n    // Each scale has its own structures.\n    setSelectedId(null)\n  }\n  return (\n    // Side by side when the component itself is wide enough, so it fits a\n    // narrow column as well as a full page.\n    <div\n      data-slot=\"condition-viewer\"\n      className={cn(\"@container min-w-0\", className)}\n    >\n      <div\n        className={cn(\n          \"grid gap-4\",\n          panel && \"@3xl:grid-cols-[minmax(0,1fr)_20rem]\"\n        )}\n      >\n        <div className=\"flex min-w-0 flex-col gap-3\">\n          <OrganViewer\n            // A new scale is a new model file.\n            key={scale.id}\n            model={model}\n            selectedId={selectedId}\n            onSelectionChange={setSelectedId}\n            label={\n              label ??\n              `Interactive ${condition.name.toLowerCase()} model${\n                condition.scales.length > 1\n                  ? `, ${scale.label.toLowerCase()}`\n                  : \"\"\n              }`\n            }\n            {...props}\n          />\n          {timeline && stage && (condition.stages?.length ?? 0) > 1 && (\n            <ConditionTimeline\n              condition={condition}\n              stage={stage}\n              onStageChange={setStageId}\n            />\n          )}\n        </div>\n        {panel && (\n          <ConditionPanel\n            condition={condition}\n            model={model}\n            scale={scale}\n            stage={stage}\n            selectedId={selectedId}\n            onScaleChange={changeScale}\n            onSelectionChange={setSelectedId}\n          />\n        )}\n      </div>\n    </div>\n  )\n}\n\nfunction ConditionPanel({\n  condition,\n  model,\n  scale,\n  stage,\n  selectedId,\n  onScaleChange,\n  onSelectionChange,\n}: {\n  condition: Condition\n  model: OrganModel\n  scale: ConditionScale\n  stage?: ConditionStage\n  selectedId: string | null\n  onScaleChange: (scale: string) => void\n  onSelectionChange: (id: string | null) => void\n}) {\n  const title = React.useId()\n  const investigations = stage?.investigations ?? condition.investigations ?? []\n  return (\n    // Beside the model, the panel takes the height the model and timeline\n    // set, rather than setting it, and scrolls.\n    <section\n      aria-labelledby={title}\n      className=\"flex min-w-0 flex-col gap-5 rounded-xl border p-4 text-sm @3xl:h-0 @3xl:min-h-full @3xl:overflow-y-auto\"\n    >\n      <div className=\"flex flex-col gap-1.5\">\n        <div className=\"flex items-start justify-between gap-3\">\n          <p id={title} className=\"leading-snug font-semibold\">\n            {condition.name}\n          </p>\n          <Badge variant=\"outline\" className=\"shrink-0 font-mono\">\n            <span className=\"sr-only\">ICD-10 </span>\n            {condition.code}\n          </Badge>\n        </div>\n        <p className=\"text-muted-foreground\">{condition.summary}</p>\n      </div>\n      {condition.scales.length > 1 && (\n        <Choice\n          label=\"Scale\"\n          options={condition.scales}\n          value={scale.id}\n          onChange={onScaleChange}\n        />\n      )}\n      <Findings\n        // Each scale starts with its spared structures folded.\n        key={scale.id}\n        model={model}\n        noted={\n          new Set(\n            Object.keys({\n              ...scale.findings,\n              ...stage?.findings?.[scale.id],\n            })\n          )\n        }\n        selectedId={selectedId}\n        onSelectionChange={onSelectionChange}\n      />\n      {investigations.length > 0 && (\n        <Investigations investigations={investigations} />\n      )}\n    </section>\n  )\n}\n\n// The course as a timeline: each stage at its typical time, the events along\n// it, and play to step through. The model recolours as the stage changes.\nfunction ConditionTimeline({\n  condition,\n  stage,\n  onStageChange,\n}: {\n  condition: Condition\n  stage: ConditionStage\n  onStageChange: (stage: string) => void\n}) {\n  const stages = React.useMemo(() => condition.stages ?? [], [condition.stages])\n  const index = stages.findIndex(({ id }) => id === stage.id)\n  const [playing, setPlaying] = React.useState(false)\n  const [event, setEvent] = React.useState<ConditionEvent | null>(null)\n  // Each stage shows for a few seconds, then the next; playing stops on the\n  // last stage of the course.\n  React.useEffect(() => {\n    const next = stages[index + 1]\n    if (!playing || !next) return\n    const timer = setTimeout(() => {\n      onStageChange(next.id)\n      if (index + 2 === stages.length) setPlaying(false)\n    }, 2500)\n    return () => clearTimeout(timer)\n  }, [playing, index, stages, onStageChange])\n  const choose = (id: string) => {\n    setPlaying(false)\n    setEvent(null)\n    onStageChange(id)\n  }\n  const shown = event?.stage === stage.id ? event : null\n  // The stages are one tab stop: the arrow keys, Home and End move along the\n  // course and show each stage on the way.\n  const buttons = React.useRef<(HTMLButtonElement | null)[]>([])\n  const onKeyDown = (keyEvent: React.KeyboardEvent) => {\n    const last = stages.length - 1\n    const next = {\n      ArrowRight: index + 1,\n      ArrowDown: index + 1,\n      ArrowLeft: index - 1,\n      ArrowUp: index - 1,\n      Home: 0,\n      End: last,\n    }[keyEvent.key]\n    if (next === undefined) return\n    keyEvent.preventDefault()\n    const target = Math.min(Math.max(next, 0), last)\n    choose(stages[target]!.id)\n    buttons.current[target]?.focus()\n  }\n  // The stages on the way to the current one. An alternative stage follows\n  // the stage its siblings follow, not the sibling drawn before it.\n  const path = new Set<number>()\n  for (let i = index; i >= 0;) {\n    path.add(i)\n    let first = i\n    while (first > 0 && stages[first]!.alternative) first--\n    i = first - 1\n  }\n  return (\n    <div\n      data-slot=\"condition-timeline\"\n      className=\"@container flex min-w-0 flex-col gap-3 rounded-xl border p-3 text-sm\"\n    >\n      <div className=\"flex items-start gap-2\">\n        <Button\n          type=\"button\"\n          variant=\"outline\"\n          size=\"icon-sm\"\n          aria-label={playing ? \"Pause the course\" : \"Play the course\"}\n          onClick={() => {\n            setEvent(null)\n            // Playing from the end starts the course again.\n            if (!playing && index === stages.length - 1)\n              onStageChange(stages[0]!.id)\n            setPlaying(!playing)\n          }}\n          className=\"mt-0.5 shrink-0\"\n        >\n          {playing ? (\n            <IconPlaceholder\n              lucide=\"PauseIcon\"\n              tabler=\"IconPlayerPause\"\n              hugeicons=\"PauseIcon\"\n              phosphor=\"PauseIcon\"\n              remixicon=\"RiPauseLine\"\n              aria-hidden=\"true\"\n            />\n          ) : (\n            <IconPlaceholder\n              lucide=\"PlayIcon\"\n              tabler=\"IconPlayerPlay\"\n              hugeicons=\"PlayIcon\"\n              phosphor=\"PlayIcon\"\n              remixicon=\"RiPlayLine\"\n              aria-hidden=\"true\"\n            />\n          )}\n        </Button>\n        {/* Down the side in a narrow space, such as a phone; across once\n            each stage has room for its name. */}\n        <ol\n          aria-label=\"Course\"\n          className=\"flex min-w-0 flex-1 flex-col @lg:grid\"\n          style={{\n            gridTemplateColumns: `repeat(${stages.length}, minmax(0, 1fr))`,\n          }}\n        >\n          {stages.map((item, i) => {\n            const current = i === index\n            const reached = path.has(i)\n            const events = (condition.events ?? []).filter(\n              (entry) => entry.stage === item.id\n            )\n            return (\n              <li\n                key={item.id}\n                className=\"flex min-w-0 gap-2 @lg:flex-col @lg:gap-1\"\n              >\n                {/* The track: a line in from the previous stage, a dot, and a\n                    line out to the next. An alternative stage is dashed. */}\n                <div\n                  aria-hidden=\"true\"\n                  className=\"flex w-3 shrink-0 flex-col items-center @lg:h-3 @lg:w-auto @lg:flex-row\"\n                >\n                  <Segment\n                    edge=\"in\"\n                    hidden={i === 0}\n                    reached={reached && !item.alternative && path.has(i - 1)}\n                    dashed={item.alternative}\n                  />\n                  <span\n                    className={cn(\n                      \"size-2.5 shrink-0 rounded-full border-2 border-foreground/30 bg-background transition-colors\",\n                      reached && \"border-foreground\",\n                      current && \"bg-foreground\"\n                    )}\n                  />\n                  <Segment\n                    edge=\"out\"\n                    hidden={i === stages.length - 1}\n                    reached={\n                      reached && path.has(i + 1) && !stages[i + 1]!.alternative\n                    }\n                    dashed={stages[i + 1]?.alternative}\n                  />\n                </div>\n                <div className=\"flex min-w-0 flex-1 flex-col items-start gap-1 pb-3 @lg:items-center @lg:pb-0\">\n                  <button\n                    ref={(button) => {\n                      buttons.current[i] = button\n                    }}\n                    type=\"button\"\n                    aria-pressed={current}\n                    tabIndex={current ? 0 : -1}\n                    onClick={() => choose(item.id)}\n                    onKeyDown={onKeyDown}\n                    className=\"flex max-w-full flex-col items-start rounded-md px-1 py-0.5 text-left transition-colors hover:bg-muted focus-visible:outline-2 focus-visible:outline-ring @lg:items-center @lg:text-center\"\n                  >\n                    <span\n                      className={cn(\n                        \"leading-snug break-words hyphens-auto text-muted-foreground\",\n                        current && \"font-medium text-foreground\"\n                      )}\n                    >\n                      {item.alternative && (\n                        <span className=\"font-normal text-muted-foreground\">\n                          or{\" \"}\n                        </span>\n                      )}\n                      {item.label}\n                    </span>\n                    {item.time && (\n                      <span className=\"text-xs text-muted-foreground\">\n                        {item.time}\n                      </span>\n                    )}\n                  </button>\n                  {events.length > 0 && (\n                    <ul className=\"flex max-w-full flex-col items-start gap-0.5 @lg:items-center\">\n                      {events.map((entry) => (\n                        <li key={entry.label}>\n                          <button\n                            type=\"button\"\n                            aria-pressed={shown === entry}\n                            onClick={() => {\n                              setPlaying(false)\n                              onStageChange(entry.stage)\n                              setEvent(shown === entry ? null : entry)\n                            }}\n                            className=\"flex max-w-full items-center gap-1.5 rounded-md px-1.5 py-0.5 text-left text-xs break-words hyphens-auto text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-2 focus-visible:outline-ring aria-pressed:bg-muted aria-pressed:text-foreground\"\n                          >\n                            <span\n                              aria-hidden=\"true\"\n                              className=\"size-1.5 shrink-0 rotate-45 bg-current\"\n                            />\n                            {entry.label}\n                          </button>\n                        </li>\n                      ))}\n                    </ul>\n                  )}\n                </div>\n              </li>\n            )\n          })}\n        </ol>\n      </div>\n      <p aria-live=\"polite\" className=\"text-muted-foreground\">\n        {shown ? (\n          <>\n            <span className=\"font-medium text-foreground\">\n              {shown.label}\n              {shown.time && `, ${shown.time.toLowerCase()}`}:\n            </span>{\" \"}\n            {shown.description}\n          </>\n        ) : (\n          <>\n            <span className=\"font-medium text-foreground\">{stage.label}:</span>{\" \"}\n            {stage.summary}\n          </>\n        )}\n      </p>\n    </div>\n  )\n}\n\n// One side of a stage's dot. Down the side, the line in is a short stub that\n// meets the line out of the stage above, which runs the length of its row.\nfunction Segment({\n  edge,\n  hidden,\n  reached,\n  dashed,\n}: {\n  edge: \"in\" | \"out\"\n  hidden: boolean\n  reached: boolean\n  dashed?: boolean\n}) {\n  return (\n    <span\n      className={cn(\n        \"w-0 border-l-2 border-foreground/15 transition-colors @lg:h-0 @lg:w-auto @lg:flex-1 @lg:border-t-2 @lg:border-l-0\",\n        edge === \"in\" ? \"h-1.5\" : \"flex-1\",\n        reached && \"border-foreground\",\n        dashed && \"border-dashed\",\n        hidden && \"invisible\"\n      )}\n    />\n  )\n}\n\nfunction Choice({\n  label,\n  options,\n  value,\n  onChange,\n}: {\n  label: string\n  options: readonly { id: string; label: string }[]\n  value: string\n  onChange: (value: string) => void\n}) {\n  const id = React.useId()\n  return (\n    <div className=\"flex flex-col gap-1.5\">\n      <span id={id} className=\"text-xs font-medium text-muted-foreground\">\n        {label}\n      </span>\n      <div\n        role=\"group\"\n        aria-labelledby={id}\n        className=\"flex flex-wrap gap-0.5 rounded-lg bg-muted p-[3px]\"\n      >\n        {options.map((option) => (\n          <Button\n            key={option.id}\n            type=\"button\"\n            size=\"sm\"\n            variant=\"ghost\"\n            aria-pressed={option.id === value}\n            onClick={() => onChange(option.id)}\n            className=\"h-7 flex-1 px-2 text-foreground/60 aria-pressed:bg-background aria-pressed:text-foreground aria-pressed:shadow-sm dark:text-muted-foreground\"\n          >\n            {option.label}\n          </Button>\n        ))}\n      </div>\n    </div>\n  )\n}\n\nfunction Findings({\n  model,\n  noted,\n  selectedId,\n  onSelectionChange,\n}: {\n  model: OrganModel\n  noted: ReadonlySet<string>\n  selectedId: string | null\n  onSelectionChange: (id: string | null) => void\n}) {\n  const list = React.useRef<HTMLDivElement>(null)\n  const [expanded, setExpanded] = React.useState(false)\n  // A structure picked in 3D scrolls its finding into view.\n  React.useEffect(() => {\n    if (!selectedId) return\n    list.current\n      ?.querySelector(`[data-structure=\"${CSS.escape(selectedId)}\"]`)\n      ?.scrollIntoView({ block: \"nearest\" })\n  }, [selectedId])\n  const row = (structure: OrganModel[\"structures\"][number]) => {\n    const selected = structure.id === selectedId\n    const affected = structure.group === \"Affected\"\n    return (\n      <li key={structure.id} data-structure={structure.id}>\n        <button\n          type=\"button\"\n          aria-pressed={selected}\n          onClick={() => onSelectionChange(selected ? null : structure.id)}\n          className=\"flex w-full items-center gap-2.5 rounded-md px-2 py-1.5 text-left transition-colors hover:bg-muted focus-visible:outline-2 focus-visible:outline-ring aria-pressed:bg-muted\"\n        >\n          <span\n            aria-hidden=\"true\"\n            className=\"size-2.5 shrink-0 rounded-full\"\n            style={{ backgroundColor: structure.color }}\n          />\n          <span className={cn(\"min-w-0 flex-1\", affected && \"font-medium\")}>\n            {structure.label}\n          </span>\n        </button>\n        {selected && structure.description && (\n          <p className=\"pt-1 pr-2 pb-2 pl-7 text-muted-foreground\">\n            {structure.description}\n          </p>\n        )}\n      </li>\n    )\n  }\n  const affected = model.structures.filter(({ group }) => group === \"Affected\")\n  const spared = model.structures.filter(({ group }) => group === \"Spared\")\n  // Spared structures the condition says something about come first; the rest\n  // of the model folds away, unless one of them is selected.\n  const shown = spared.filter(({ id }) => noted.has(id))\n  const folded = spared.filter(({ id }) => !noted.has(id))\n  const open = expanded || folded.some(({ id }) => id === selectedId)\n  return (\n    <div ref={list} className=\"flex flex-col gap-4\">\n      {affected.length > 0 && (\n        <div className=\"flex flex-col gap-1\">\n          <span className=\"text-xs font-medium text-muted-foreground\">\n            Affected · {affected.length}\n          </span>\n          <ul className=\"-mx-2 flex flex-col\">{affected.map(row)}</ul>\n        </div>\n      )}\n      {spared.length > 0 && (\n        <div className=\"flex flex-col gap-1\">\n          <span className=\"text-xs font-medium text-muted-foreground\">\n            Spared · {spared.length}\n          </span>\n          <ul className=\"-mx-2 flex flex-col\">\n            {shown.map(row)}\n            {open && folded.map(row)}\n          </ul>\n          {folded.length > 0 && (\n            <Button\n              type=\"button\"\n              variant=\"ghost\"\n              size=\"sm\"\n              aria-expanded={open}\n              onClick={() => setExpanded(!open)}\n              className=\"-mx-2 h-7 self-start px-2 text-muted-foreground\"\n            >\n              {open\n                ? \"Show fewer\"\n                : `Show ${folded.length} more ${folded.length === 1 ? \"structure\" : \"structures\"}`}\n            </Button>\n          )}\n        </div>\n      )}\n    </div>\n  )\n}\n\nconst flagLabels = { high: \"High\", low: \"Low\", abnormal: \"Abnormal\" }\n\nfunction Investigations({\n  investigations,\n}: {\n  investigations: readonly ConditionInvestigation[]\n}) {\n  const id = React.useId()\n  return (\n    <div className=\"flex flex-col gap-1.5\">\n      <span id={id} className=\"text-xs font-medium text-muted-foreground\">\n        Investigations\n      </span>\n      <dl\n        aria-labelledby={id}\n        className=\"grid grid-cols-[minmax(min-content,1fr)_auto] gap-x-3 gap-y-1.5\"\n      >\n        {investigations.map(({ name, result, flag }) => (\n          <React.Fragment key={name}>\n            <dt className=\"text-muted-foreground\">{name}</dt>\n            <dd\n              className={cn(\n                \"flex items-center justify-end gap-1 text-right\",\n                flag && \"font-semibold\"\n              )}\n            >\n              {result}\n              {flag && (\n                <>\n                  <FlagIcon flag={flag} />\n                  <span className=\"sr-only\">({flagLabels[flag]})</span>\n                </>\n              )}\n            </dd>\n          </React.Fragment>\n        ))}\n      </dl>\n    </div>\n  )\n}\n\nfunction FlagIcon({\n  flag,\n}: {\n  flag: NonNullable<ConditionInvestigation[\"flag\"]>\n}) {\n  if (flag === \"high\")\n    return (\n      <IconPlaceholder\n        lucide=\"ArrowUpIcon\"\n        tabler=\"IconArrowUp\"\n        hugeicons=\"ArrowUpIcon\"\n        phosphor=\"ArrowUpIcon\"\n        remixicon=\"RiArrowUpLine\"\n        aria-hidden=\"true\"\n        className=\"size-4 shrink-0\"\n      />\n    )\n  if (flag === \"low\")\n    return (\n      <IconPlaceholder\n        lucide=\"ArrowDownIcon\"\n        tabler=\"IconArrowDown\"\n        hugeicons=\"ArrowDownIcon\"\n        phosphor=\"ArrowDownIcon\"\n        remixicon=\"RiArrowDownLine\"\n        aria-hidden=\"true\"\n        className=\"size-4 shrink-0\"\n      />\n    )\n  return (\n    <IconPlaceholder\n      lucide=\"TriangleAlertIcon\"\n      tabler=\"IconAlertTriangle\"\n      hugeicons=\"Alert02Icon\"\n      phosphor=\"WarningIcon\"\n      remixicon=\"RiErrorWarningLine\"\n      aria-hidden=\"true\"\n      className=\"size-4 shrink-0\"\n    />\n  )\n}\n",
      "type": "registry:component"
    },
    {
      "path": "registry/base-nova/condition-viewer/condition-model.ts",
      "content": "import type { OrganModel } from \"@/components/organ-viewer\"\n\n// The data a condition is made of, and the organ model it shows at each scale\n// and stage. Plain data and functions, so server code can use them too.\n\nexport type ConditionFinding = {\n  /** Whether the condition involves the structure. */\n  affected: boolean\n  /** Replaces the structure's anatomical label, such as \"Portal tracts\". */\n  label?: string\n  /** What the condition does to the structure, or why it is spared. */\n  description: string\n}\n\nexport type ConditionScale = {\n  /** The name of the organ or tissue item the model comes from, such as \"liver\". */\n  id: string\n  /** The name the scale switch shows, such as \"Liver\" or \"Lobule\". */\n  label: string\n  model: OrganModel\n  /** The named view the model opens on, where the condition shows best. Defaults to the model's own. */\n  defaultView?: string\n  /** Structures drawn see-through so the condition inside them shows, such as the fat over a breast's lobes. Defaults to the model's own. */\n  translucentIds?: readonly string[]\n  /** Findings by structure ID. Structures without one are spared and keep their anatomical description. */\n  findings: Readonly<Record<string, ConditionFinding>>\n}\n\nexport type ConditionInvestigation = {\n  name: string\n  result: string\n  /** The direction of an abnormal result, shown bold with an arrow. */\n  flag?: \"high\" | \"low\" | \"abnormal\"\n}\n\nexport type ConditionStage = {\n  id: string\n  label: string\n  /** When the stage comes in a typical course, such as \"Days 3–7\". */\n  time?: string\n  /** Marks a stage that is an alternative to the one before it, not what follows it, such as an untreated course beside a treated one. */\n  alternative?: boolean\n  /** What happens at this stage, in a sentence or two. */\n  summary: string\n  /** Findings that replace the scale's own at this stage, by scale ID, then structure ID. */\n  findings?: Readonly<\n    Record<string, Readonly<Record<string, ConditionFinding>>>\n  >\n  /** Replaces the condition's investigations at this stage. */\n  investigations?: readonly ConditionInvestigation[]\n}\n\nexport type ConditionEvent = {\n  /** The stage it happens in, by ID. */\n  stage: string\n  /** A short name for the timeline, such as \"PCI\". */\n  label: string\n  /** When it happens, such as \"Within 120 min\". */\n  time?: string\n  /** What it is and why it matters then. */\n  description: string\n}\n\nexport type Condition = {\n  name: string\n  /** The ICD-10 code. */\n  code: string\n  summary: string\n  /** One model per scale, the default first. */\n  scales: readonly ConditionScale[]\n  /** The course of the condition, the default first. */\n  stages?: readonly ConditionStage[]\n  /** Treatments, tests and turning points along the course, in order. */\n  events?: readonly ConditionEvent[]\n  investigations?: readonly ConditionInvestigation[]\n}\n\n// Affected structures share one amber, so a condition that involves a whole\n// organ still reads as affected, and spared ones one muted tone. Amber, not\n// red: red is kept for critical results.\nexport const AFFECTED = \"#e0892f\"\nexport const SPARED = \"#a9a59c\"\n\nexport function scaleOf(condition: Condition, id?: string) {\n  return (\n    condition.scales.find((scale) => scale.id === id) ?? condition.scales[0]!\n  )\n}\n\nexport function stageOf(condition: Condition, id?: string) {\n  return (\n    condition.stages?.find((stage) => stage.id === id) ?? condition.stages?.[0]\n  )\n}\n\n/**\n * One scale of a condition at one stage, as an organ model: the base model's\n * GLB, views and credit, with its structures grouped as affected or spared,\n * the affected ones first.\n */\nexport function conditionModel(\n  condition: Condition,\n  scaleId?: string,\n  stageId?: string\n): OrganModel {\n  const scale = scaleOf(condition, scaleId)\n  const findings = {\n    ...scale.findings,\n    ...stageOf(condition, stageId)?.findings?.[scale.id],\n  }\n  return {\n    ...scale.model,\n    name: condition.name,\n    defaultView: scale.defaultView ?? scale.model.defaultView,\n    translucentIds: scale.translucentIds ?? scale.model.translucentIds,\n    structures: scale.model.structures\n      .map((structure) => {\n        const finding = findings[structure.id]\n        return {\n          ...structure,\n          label: finding?.label ?? structure.label,\n          group: finding?.affected ? \"Affected\" : \"Spared\",\n          color: finding?.affected ? AFFECTED : SPARED,\n          description: finding?.description ?? structure.description,\n        }\n      })\n      .sort(\n        (a, b) =>\n          Number(b.group === \"Affected\") - Number(a.group === \"Affected\")\n      ),\n  }\n}\n",
      "type": "registry:component"
    }
  ],
  "docs": "Condition viewer docs: https://organui.com/docs/components/condition-viewer. Educational, not for diagnosis.",
  "categories": [
    "pathology"
  ],
  "type": "registry:component"
}