{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "eased-health-core",
  "title": "Health core primitive",
  "description": "Shared pieces for Eased health blocks: where a value sits against its range, a warning flag that pops in and out, and clinic clock times.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://eased.dev/r/eased-tokens.json",
    "https://eased.dev/r/eased-motion.json",
    "https://eased.dev/r/eased-text-swap.json"
  ],
  "files": [
    {
      "path": "src/registry/eased/_primitives/health-core/health-core.tsx",
      "content": "\"use client\";\n\n/**\n * health-core: the pieces every Eased health block shares, so vitals, labs and schedules read\n * as one product: where a value sits against its range, the flag that says so, and clinic\n * clock times. Out of range reads `warning`, never `danger`: it is information for the\n * patient and their care team, not an error (rule 119). Import from here.\n */\nimport { AnimatePresence, motion } from \"motion/react\";\nimport { exitFast, reducedFade, snappy, useSafeReducedMotion } from \"../../lib/motion\";\nimport { TextSwap } from \"../text-swap/text-swap\";\n\n/** Default locale and clinic time zone for every health format. Pass your own. */\nexport const HEALTH_LOCALE = \"en-US\";\nexport const HEALTH_TIME_ZONE = \"UTC\";\n\n/* ---------- Ranges ----------------------------------------------------------------------- */\n\nexport type RangeStatus = \"low\" | \"normal\" | \"high\";\nexport type Range = { low: number; high: number };\n\n/** Where `value` sits against a reference range (inclusive at both ends). */\nexport function rangeStatus(value: number, { low, high }: Range): RangeStatus {\n  return value < low ? \"low\" : value > high ? \"high\" : \"normal\";\n}\n\n/** Text colour for a value in this state: in range stays `text-text`, out of range is `warning`. */\nexport const rangeText = (status: RangeStatus) => (status === \"normal\" ? \"text-text\" : \"text-warning-text\");\n\nconst FLAG_LABEL: Record<RangeStatus, string> = { low: \"Low\", normal: \"In range\", high: \"High\" };\n\nexport type RangeFlagProps = {\n  status: RangeStatus;\n  /** Show the \"In range\" chip too. Default false: only flags appear, and they pop in and out. */\n  showNormal?: boolean;\n  /** Override the labels, e.g. `{ high: \"Above range\" }`. */\n  labels?: Partial<Record<RangeStatus, string>>;\n  className?: string;\n};\n\n/**\n * A 20px flag for a value outside its range. It pops in on the snappy spring when a value\n * leaves the range and shrinks away when it returns; between Low and High its label swaps.\n */\nexport function RangeFlag({ status, showNormal = false, labels, className = \"\" }: RangeFlagProps) {\n  const reduced = useSafeReducedMotion();\n  const shown = status !== \"normal\" || showNormal;\n  const label = labels?.[status] ?? FLAG_LABEL[status];\n  const tone = status === \"normal\" ? \"bg-success-subtle text-success-text\" : \"bg-warning-subtle text-warning-text\";\n  return (\n    <AnimatePresence initial={false}>\n      {shown && (\n        <motion.span\n          initial={reduced ? { opacity: 0 } : { opacity: 0, scale: 0.85 }}\n          animate={{ opacity: 1, scale: 1 }}\n          exit={reduced ? { opacity: 0, transition: reducedFade } : { opacity: 0, scale: 0.9, transition: exitFast }}\n          transition={reduced ? reducedFade : snappy}\n          className={`inline-flex h-5 shrink-0 items-center rounded-control-xs px-1.5 text-ui-xs font-medium whitespace-nowrap transition-colors duration-150 ease-standard ${tone} ${className}`}\n        >\n          <TextSwap>{label}</TextSwap>\n        </motion.span>\n      )}\n    </AnimatePresence>\n  );\n}\n\n/* ---------- Clinic time ------------------------------------------------------------------ */\n\n/** \"08:30\" → 510 minutes after midnight. */\nexport const toMinutes = (hhmm: string) => {\n  const [h, m] = hhmm.split(\":\").map(Number);\n  return h * 60 + (m || 0);\n};\n\nconst timeFmt = new Map<string, Intl.DateTimeFormat>();\n/** Epoch ms → \"8:30 AM\" in the clinic's time zone. */\nexport function formatClock(t: number, { locale = HEALTH_LOCALE, timeZone = HEALTH_TIME_ZONE }: { locale?: string; timeZone?: string } = {}) {\n  const k = `${locale}|${timeZone}`;\n  let f = timeFmt.get(k);\n  if (!f) timeFmt.set(k, (f = new Intl.DateTimeFormat(locale, { hour: \"numeric\", minute: \"2-digit\", timeZone })));\n  return f.format(t);\n}\n\n/** Minutes after midnight → \"9:00 PM\", without a date. */\nexport const formatMinutes = (minutes: number, locale = HEALTH_LOCALE) => formatClock(Date.UTC(2026, 0, 1, 0, minutes), { locale, timeZone: \"UTC\" });\n",
      "type": "registry:ui",
      "target": "@components/eased/_primitives/health-core/health-core.tsx"
    }
  ],
  "type": "registry:ui"
}