{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "eased-hr-core",
  "title": "HR core primitive",
  "description": "Shared pieces for Eased HR and recruiting blocks: the 1–4 interview scale and its labels, four pips that fill to a rating, and the short time-in-stage clock.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://eased.dev/r/eased-tokens.json",
    "https://eased.dev/r/eased-motion.json"
  ],
  "files": [
    {
      "path": "src/registry/eased/_primitives/hr-core/hr-core.tsx",
      "content": "\"use client\";\n\n/**\n * hr-core: what every Eased HR / recruiting block shares, so a pipeline, a profile and a\n * scorecard read as one product: the 1–4 interview scale, its four-pip meter, and the short\n * \"3d in stage\" clock. Import from here.\n */\nimport { motion } from \"motion/react\";\nimport { smooth, useSafeReducedMotion } from \"../../lib/motion\";\n\n/** Default locale for every HR format. Pass your own. */\nexport const HR_LOCALE = \"en-US\";\n\n/* ---------- The interview scale ---------------------------------------------------------- */\n\n/** The four points Greenhouse and Ashby use. There is no middle: a panelist has to lean. */\nexport const RATING_LABELS = [\"Strong no\", \"No\", \"Yes\", \"Strong yes\"] as const;\n\n/** \"Strong yes\" for 4, \"Yes\" for 3.4 (rounded), \"\" for no rating. */\nexport function ratingLabel(rating: number | null | undefined) {\n  if (!rating) return \"\";\n  return RATING_LABELS[Math.min(4, Math.max(1, Math.round(rating))) - 1];\n}\n\n/** Colour role of a rating: yes leans success, no leans warning (a \"no\" is information, not an error). */\nexport function ratingTone(rating: number | null | undefined): \"success\" | \"warning\" | \"neutral\" {\n  if (!rating) return \"neutral\";\n  return rating >= 2.5 ? \"success\" : \"warning\";\n}\n\nconst PIP_FILL = { success: \"bg-success\", warning: \"bg-warning\", neutral: \"bg-text-muted\" } as const;\n\nexport type RatingPipsProps = {\n  /** 1–4, fractional for a mean (3.4 fills three and a half pips). Null: empty. */\n  value: number | null | undefined;\n  /** Spoken label, e.g. \"Jonas Lindqvist\". The value and its word are added. */\n  label?: string;\n  locale?: Intl.LocalesArgument;\n  /** Set the width here (default `w-16`); the pips share it. */\n  className?: string;\n};\n\n/**\n * Four pips that fill to a rating: each pip's fill slides in on the smooth spring (a\n * transform, so a half pip keeps its rounded end), tinted success for yes and warning for no.\n * A `meter` with the value and its word spoken.\n */\nexport function RatingPips({ value, label, locale = HR_LOCALE, className = \"w-16\" }: RatingPipsProps) {\n  const reduced = useSafeReducedMotion();\n  const v = value ?? 0;\n  const fill = PIP_FILL[ratingTone(value)];\n  const nf = new Intl.NumberFormat(locale, { maximumFractionDigits: 1 });\n  return (\n    <span\n      role=\"meter\"\n      aria-label={label}\n      aria-valuemin={1}\n      aria-valuemax={4}\n      aria-valuenow={value ?? undefined}\n      aria-valuetext={value ? `${nf.format(v)} of 4, ${ratingLabel(v)}` : \"Not rated\"}\n      className={`inline-grid shrink-0 grid-cols-4 gap-0.5 ${className}`}\n    >\n      {[0, 1, 2, 3].map((i) => {\n        const share = Math.min(1, Math.max(0, v - i));\n        return (\n          <span key={i} aria-hidden className=\"relative h-1.5 overflow-hidden rounded-pill bg-surface-active\">\n            <motion.span\n              className={`absolute inset-0 rounded-pill transition-colors duration-200 ease-standard ${fill}`}\n              initial={false}\n              animate={{ x: `${(share - 1) * 100}%` }}\n              transition={reduced ? { duration: 0 } : { ...smooth, delay: share > 0 ? i * 0.03 : (3 - i) * 0.03 }}\n            />\n          </span>\n        );\n      })}\n    </span>\n  );\n}\n\n/* ---------- Time in stage ---------------------------------------------------------------- */\n\n/** \"5h\", \"3d\", \"2w\": how long something has sat in a stage, for dense cards. */\nexport function timeIn(since: number, now: number) {\n  const h = Math.max(0, (now - since) / 3_600_000);\n  if (h < 24) return `${Math.max(1, Math.round(h))}h`;\n  const d = Math.round(h / 24);\n  return d < 14 ? `${d}d` : `${Math.round(d / 7)}w`;\n}\n\n/** \"5 hours\", \"3 days\", for the spoken copy of `timeIn`. */\nexport function timeInLong(since: number, now: number) {\n  const short = timeIn(since, now);\n  const n = Number.parseInt(short, 10);\n  const unit = short.endsWith(\"h\") ? \"hour\" : short.endsWith(\"d\") ? \"day\" : \"week\";\n  return `${n} ${unit}${n === 1 ? \"\" : \"s\"}`;\n}\n",
      "type": "registry:ui",
      "target": "@components/eased/_primitives/hr-core/hr-core.tsx"
    }
  ],
  "type": "registry:ui"
}