{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "mastery",
  "title": "Mastery meter",
  "description": "Per-skill mastery bars plus a learner self-rating, emitting an xAPI rated statement.",
  "registryDependencies": [
    "https://beadkit.dev/r/learning-core.json"
  ],
  "files": [
    {
      "path": "components/learning/MasteryMeter.tsx",
      "content": "\"use client\";\nimport { useState } from \"react\";\nimport { LearningCard } from \"./LearningCard\";\nimport { useTelemetry, type XApiStatement } from \"./xapi\";\n\nexport interface MasterySkill {\n  id: string;\n  label: string;\n  /** The model's assessment of the learner, 0 to 1. */\n  level: number;\n}\n\nexport interface MasteryMeterProps {\n  activityId: string;\n  title: string;\n  skills: MasterySkill[];\n  onTrackTelemetry?: (statement: XApiStatement) => void;\n}\n\nconst RATINGS = [\n  { id: \"shaky\", label: \"Still shaky\" },\n  { id: \"getting-there\", label: \"Getting there\" },\n  { id: \"solid\", label: \"Solid\" },\n];\n\n/** Segments per bar. Discrete reads as a level; a continuous fill reads as a\n *  loading state, which is the wrong idea for a mastery figure. */\nconst SEGMENTS = 5;\n\nexport function MasteryMeter({\n  activityId,\n  title,\n  skills,\n  onTrackTelemetry,\n}: MasteryMeterProps) {\n  const [rated, setRated] = useState<string | null>(null);\n  const track = useTelemetry(activityId, title, onTrackTelemetry);\n\n  const overall = skills.reduce((sum, s) => sum + s.level, 0) / skills.length;\n\n  const rate = (id: string) => {\n    if (rated) return;\n    setRated(id);\n    // No `success`: a self-rating is the learner's own read, not a scored\n    // attempt, and an LRS treats a present `success` as one.\n    track(\"rated\", { response: id });\n  };\n\n  return (\n    <LearningCard label=\"Where you are\" type=\"mastery\">\n      <div className=\"mb-3 flex items-baseline justify-between gap-3\">\n        <p className=\"text-sm font-semibold\">{title}</p>\n        <span className=\"font-mono text-sm font-semibold\">{Math.round(overall * 100)}%</span>\n      </div>\n\n      <ul className=\"space-y-2.5\">\n        {skills.map((skill) => {\n          const filled = Math.round(skill.level * SEGMENTS);\n          return (\n            <li key={skill.id}>\n              <div className=\"mb-1 flex items-baseline justify-between gap-3\">\n                <span className=\"text-xs\">{skill.label}</span>\n                <span className=\"text-muted-foreground font-mono text-[11px]\">\n                  {Math.round(skill.level * 100)}%\n                </span>\n              </div>\n              <div\n                className=\"flex gap-1\"\n                role=\"meter\"\n                aria-valuenow={filled}\n                aria-valuemin={0}\n                aria-valuemax={SEGMENTS}\n                aria-label={skill.label}\n              >\n                {Array.from({ length: SEGMENTS }, (_, i) => (\n                  <span\n                    key={i}\n                    // `bg-muted` is identical to `bg-card` in the dark palette,\n                    // so the empty track has to come from a foreground alpha to\n                    // stay visible in both modes.\n                    className={`h-1.5 flex-1 rounded-full ${\n                      i < filled ? \"bg-primary\" : \"bg-muted-foreground/25\"\n                    }`}\n                  />\n                ))}\n              </div>\n            </li>\n          );\n        })}\n      </ul>\n\n      <fieldset className=\"mt-4\" disabled={rated !== null}>\n        <legend className=\"text-muted-foreground mb-2 text-xs\">\n          How solid does this feel to you?\n        </legend>\n        <div className=\"flex gap-1.5\">\n          {RATINGS.map((r) => (\n            <label\n              key={r.id}\n              className={`has-[:focus-visible]:ring-ring flex-1 cursor-pointer rounded-lg border p-2 text-center text-[11px] transition-all has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-offset-1 ${\n                rated === r.id\n                  ? \"border-primary bg-primary/5 font-medium\"\n                  : \"hover:border-muted-foreground/40\"\n              } ${rated ? \"cursor-default\" : \"\"}`}\n            >\n              <input\n                type=\"radio\"\n                name={`${activityId}-rating`}\n                value={r.id}\n                checked={rated === r.id}\n                onChange={() => rate(r.id)}\n                className=\"sr-only\"\n              />\n              {r.label}\n            </label>\n          ))}\n        </div>\n      </fieldset>\n\n      <div aria-live=\"polite\">\n        {rated && (\n          <p className=\"text-muted-foreground mt-3 text-xs\">\n            Noted — your tutor will weight the next questions accordingly.\n          </p>\n        )}\n      </div>\n    </LearningCard>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/learning/MasteryMeter.tsx"
    }
  ],
  "type": "registry:component"
}
