{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "team-challenge",
  "title": "Team Challenge",
  "author": "ilinxa",
  "description": "Cooperative team challenge card — one shared goal, collective progress, a whole-team reward, and penalty-free opt-in.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "progress",
    "avatar",
    "button",
    "badge",
    "skeleton",
    "@ilinxa/gamification-kit"
  ],
  "files": [
    {
      "path": "src/registry/components/gamification/team-challenge/team-challenge.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { TeamChallengeHeader } from \"./parts/team-challenge-header\";\nimport { TeamChallengeOptIn } from \"./parts/team-challenge-optin\";\nimport { TeamChallengeProgress } from \"./parts/team-challenge-progress\";\nimport { TeamChallengeReward } from \"./parts/team-challenge-reward\";\nimport { TeamChallengeRoot } from \"./parts/team-challenge-root\";\nimport type { TeamChallengeProps } from \"./types\";\n\n/**\n * Tier A — the batteries-included assembly: `Root` + `Header` + `Progress` +\n * `Reward?` + `OptIn?`, gated by `show*`. Contains **no logic the parts don't**\n * — a hand-assembled layout (`TeamChallengeRoot` + a subset of parts)\n * gets identical derivation, identical controlled-echo, and identical telemetry.\n *\n * A safe-by-design cooperative challenge: one shared team goal, collective\n * progress, a whole-team reward, and a penalty-free opt-in. Never forced, never\n * per-individual, never competitive (system §5).\n */\nexport function TeamChallenge({\n  challenge,\n  team,\n  onOptInChange,\n  onEvent,\n  showOptIn = true,\n  showReward = true,\n  showMemberStack = true,\n  joinLabel,\n  leaveLabel,\n  noPenaltyHint,\n  className,\n  \"aria-label\": ariaLabel,\n}: TeamChallengeProps) {\n  return (\n    <TeamChallengeRoot\n      challenge={challenge}\n      team={team}\n      onOptInChange={onOptInChange}\n      onEvent={onEvent}\n      joinLabel={joinLabel}\n      leaveLabel={leaveLabel}\n      noPenaltyHint={noPenaltyHint}\n      className={className}\n      aria-label={ariaLabel}\n    >\n      <TeamChallengeHeader showMemberStack={showMemberStack} />\n      <TeamChallengeProgress />\n      {showReward ? <TeamChallengeReward /> : null}\n      {showOptIn ? <TeamChallengeOptIn /> : null}\n    </TeamChallengeRoot>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/team-challenge/team-challenge.tsx"
    },
    {
      "path": "src/registry/components/gamification/team-challenge/index.ts",
      "content": "// Tier A — batteries-included assembly.\nexport { TeamChallenge } from \"./team-challenge\";\n\n// Tier B — headless provider + flat à-la-carte context parts.\nexport { TeamChallengeRoot } from \"./parts/team-challenge-root\";\nexport { TeamChallengeHeader } from \"./parts/team-challenge-header\";\nexport { TeamChallengeProgress } from \"./parts/team-challenge-progress\";\nexport { TeamChallengeReward } from \"./parts/team-challenge-reward\";\nexport { TeamChallengeOptIn } from \"./parts/team-challenge-optin\";\n\n// Tier C — standalone, context-free primitives.\nexport { ChallengeProgressMeter } from \"./parts/team-challenge-progress\";\nexport { ChallengeRewardChip } from \"./parts/team-challenge-reward\";\nexport { OptInToggle } from \"./parts/team-challenge-optin\";\nexport { TeamMemberStack } from \"./parts/team-member-stack\";\nexport { TeamChallengeSkeleton } from \"./parts/team-challenge-skeleton\";\n\n// Context consumer for hand-assembled layouts.\nexport { useTeamChallenge } from \"./hooks/use-team-challenge\";\n\n// Public types.\nexport type {\n  Challenge,\n  Team,\n  TeamMember,\n  GamificationEvent,\n  TeamChallengeProps,\n  TeamChallengeBaseProps,\n  TeamChallengeRootProps,\n  TeamChallengeContextValue,\n  ChallengeDerived,\n  OptInToggleProps,\n  ChallengeProgressMeterProps,\n  ChallengeRewardChipProps,\n  TeamMemberStackProps,\n} from \"./types\";\n",
      "type": "registry:component",
      "target": "components/team-challenge/index.ts"
    },
    {
      "path": "src/registry/components/gamification/team-challenge/types.ts",
      "content": "import type { ReactNode } from \"react\";\n\n/**\n * `team-challenge` — public type surface.\n *\n * Framework-free (no `\"use client\"`): importable from a server component's type\n * position. Per system D-03 every type this component needs is **re-declared\n * locally** — it imports nothing from another registry component. The\n * `gamification-system` description (§4 domain model, §6 telemetry union) is the\n * source of truth these slices are copied from, not imported. The type\n * duplication across the pack is the accepted price of registry independence\n * (system D-03/D-04); the deferred `gamification-kit` may dedupe later.\n */\n\n/**\n * A member of the owning team — identity only. Renders as an avatar in the\n * `TeamMemberStack`; never a per-member progress/contribution surface (§6.3).\n */\nexport interface TeamMember {\n  id: string;\n  displayName: string;\n  avatarUrl?: string;\n}\n\n/**\n * This team (system D-15). Renders team-identity **text** (the optional header\n * name) → takes a `team` object subset, never a bare `teamId`/`teamName` pair.\n * `id` scopes the telemetry payload; `members` feeds the avatar stack.\n */\nexport interface Team {\n  id: string;\n  name: string;\n  members: TeamMember[];\n}\n\n/**\n * ONE shared team goal (D-C1 — one challenge per component in v1). Progress is a\n * **collective** quantity only (D-C2); the reward is **whole-team** (D-08);\n * opting in is **controlled + never-forced** (D-C3/D-C6).\n */\nexport interface Challenge {\n  id: string;\n  /** Header label, e.g. \"All 5 members commit a task this morning\". Truncated + full text on `title`. */\n  label: string;\n  /** Controlled, team-level opt-in value (D-C6). Refusing has no penalty. */\n  optedIn: boolean;\n  /** Collective progress only — never a per-member breakdown (D-C2). */\n  progress: { current: number; target: number };\n  /** Whole-team reward chip (D-08). Undefined/empty → hidden. */\n  reward?: string;\n  /** Completed → lightweight inline earned treatment (D-C4). */\n  done: boolean;\n}\n\n/**\n * The local slice of the system §6 `GamificationEvent` union — **only** the two\n * events this component emits (D-03; a host wiring all six components widens the\n * type at its own telemetry layer). No SDK, no `next/*`.\n */\nexport type GamificationEvent =\n  | { type: \"challenge.opened\"; teamId: string; challengeId: string }\n  | {\n      type: \"challenge.opt-in\";\n      teamId: string;\n      challengeId: string;\n      optedIn: boolean;\n    };\n\n/** Data + behavioral surface shared by the assembly and the headless `Root`. */\nexport interface TeamChallengeBaseProps {\n  /** The shared team goal to render (one challenge in v1, D-C1). */\n  challenge: Challenge;\n  /** The team that owns the challenge — name + member stack + scope (D-15). */\n  team: Team;\n  /** Fires when the team opts in/out (controlled, D-C6). Omit → read-only card (control hides). */\n  onOptInChange?: (optedIn: boolean) => void;\n  /** Telemetry (system §6, D-07) — emits `challenge.opened` + `challenge.opt-in`. */\n  onEvent?: (event: GamificationEvent) => void;\n  /** Copy override — the opt-in affordance label. Default `\"Join this challenge\"`. */\n  joinLabel?: string;\n  /** Copy override — the opt-out affordance label. Default `\"Leave challenge\"` (penalty-free). */\n  leaveLabel?: string;\n  /** Copy override — the cost-free hint. Default `\"Optional — no penalty for sitting this one out\"`. */\n  noPenaltyHint?: string;\n}\n\n/** Props for the headless provider `TeamChallengeRoot`. */\nexport interface TeamChallengeRootProps\n  extends TeamChallengeBaseProps {\n  children: ReactNode;\n  className?: string;\n  \"aria-label\"?: string;\n}\n\n/** Props for the batteries-included `TeamChallenge` assembly. */\nexport interface TeamChallengeProps\n  extends TeamChallengeBaseProps {\n  /** Show the opt-in control. Default `true`; `false` → card body only. */\n  showOptIn?: boolean;\n  /** Show the reward chip. Default `true` (auto-hides if no reward). */\n  showReward?: boolean;\n  /** Show the member avatar stack. Default `true`. */\n  showMemberStack?: boolean;\n  className?: string;\n  \"aria-label\"?: string;\n}\n\n/** Tier-C context-free bare opt-in toggle (droppable on any surface). */\nexport interface OptInToggleProps {\n  optedIn: boolean;\n  onOptInChange?: (optedIn: boolean) => void;\n  joinLabel?: string;\n  leaveLabel?: string;\n  noPenaltyHint?: string;\n  disabled?: boolean;\n  className?: string;\n}\n\n/** Tier-C dumb collective progress meter. */\nexport interface ChallengeProgressMeterProps {\n  current: number;\n  target: number;\n  /** Earned/complete tint (signal-lime); still collective-only, no per-member surface. */\n  complete?: boolean;\n  className?: string;\n  \"aria-label\"?: string;\n}\n\n/** Tier-C dumb reward chip; `earned` switches framing (available → earned). */\nexport interface ChallengeRewardChipProps {\n  reward: string;\n  earned?: boolean;\n  className?: string;\n}\n\n/** Tier-C dumb avatar pile; `max` caps then `+N`. */\nexport interface TeamMemberStackProps {\n  members: TeamMember[];\n  /** Max avatars before a `+N` overflow chip. Default `5`. */\n  max?: number;\n  className?: string;\n}\n\n/** Pure derived state (see `lib/derive.ts`). Deterministic, no clock. */\nexport interface ChallengeDerived {\n  /** Clamped 0..1 fill fraction. */\n  progressFraction: number;\n  /** `${current} / ${target}` (mono). */\n  countLabel: string;\n  /** `!optedIn && !done` → neutral, joinable, invitation. */\n  isJoinable: boolean;\n  /** `optedIn && !done` → active, in-motion. */\n  isActive: boolean;\n  /** `done` → earned/completed (wins over `optedIn`). */\n  isComplete: boolean;\n  /** `reward` present + non-empty. */\n  hasReward: boolean;\n}\n\n/** The value every context part reads — one source of truth. */\nexport interface TeamChallengeContextValue {\n  challenge: Challenge;\n  team: Team;\n  derived: ChallengeDerived;\n  /** `true` when the opt-in control should render (a handler is wired). */\n  canOptIn: boolean;\n  /** Toggle opt-in — computes `next`, calls `onOptInChange`, emits telemetry. */\n  toggleOptIn: () => void;\n  joinLabel: string;\n  leaveLabel: string;\n  noPenaltyHint: string;\n}\n",
      "type": "registry:component",
      "target": "components/team-challenge/types.ts"
    },
    {
      "path": "src/registry/components/gamification/team-challenge/lib/derive.ts",
      "content": "import { clamp } from \"../../_shared/gamification-kit\";\nimport type { Challenge, ChallengeDerived } from \"../types\";\n\n/**\n * The pure derivation layer — SSR-deterministic (derives only from the\n * `challenge` prop; never reads `Date`, `Math.random`, layout, or env).\n * Test-ready. `clamp` moved to the shared `gamification-kit` (P3.5 / D-04)\n * once the surface was proven across ≥2 components — keep the rest of this\n * derivation standalone (system-model type duplication is intentional, D-03).\n *\n * No component-invented progress notion (system D-09 spirit): progress is\n * exactly `challenge.progress`; the card never derives it from milestones.\n */\n\n/** Default never-forced copy — one source of truth for the Root + the bare Tier-C toggle. */\nexport const DEFAULT_JOIN_LABEL = \"Join this challenge\";\nexport const DEFAULT_LEAVE_LABEL = \"Leave challenge\";\nexport const DEFAULT_NO_PENALTY_HINT =\n  \"Optional — no penalty for sitting this one out\";\n\n/**\n * Resolve every rendered value from the `challenge`. The `max(target, 1)`\n * divisor avoids NaN/Infinity on the `target === 0` degenerate input (§9); the\n * count label always shows the host's raw `current / target` (never silently\n * rewritten, even when `current > target` — the bar caps at 100%, the count\n * tells the truth).\n */\nexport function deriveChallenge(challenge: Challenge): ChallengeDerived {\n  const { current, target } = challenge.progress;\n  const progressFraction = clamp(current / Math.max(target, 1), 0, 1);\n  const isComplete = challenge.done;\n  const hasReward =\n    challenge.reward != null && challenge.reward.trim() !== \"\";\n\n  return {\n    progressFraction,\n    countLabel: `${current} / ${target}`,\n    // `done` wins over `optedIn` — a completed challenge reads as earned regardless.\n    isJoinable: !challenge.optedIn && !isComplete,\n    isActive: challenge.optedIn && !isComplete,\n    isComplete,\n    hasReward,\n  };\n}\n",
      "type": "registry:component",
      "target": "components/team-challenge/lib/derive.ts"
    },
    {
      "path": "src/registry/components/gamification/team-challenge/lib/events.ts",
      "content": "import type { Challenge, GamificationEvent, Team } from \"../types\";\n\n/**\n * The single place the two event shapes are constructed — standalone pure\n * factories (kit-extraction candidates, system §7.3), so the union stays\n * single-sourced from `types.ts` and the shapes are testable. The host adds the\n * envelope (timestamp, anonymized IDs, app variant) at its transport layer.\n */\n\n/** `challenge.opened` — first reveal/mount of the card (D-C5). */\nexport function openedEvent(team: Team, challenge: Challenge): GamificationEvent {\n  return {\n    type: \"challenge.opened\",\n    teamId: team.id,\n    challengeId: challenge.id,\n  };\n}\n\n/** `challenge.opt-in` — the team toggled opt-in; carries the new value. */\nexport function optInEvent(\n  team: Team,\n  challenge: Challenge,\n  optedIn: boolean,\n): GamificationEvent {\n  return {\n    type: \"challenge.opt-in\",\n    teamId: team.id,\n    challengeId: challenge.id,\n    optedIn,\n  };\n}\n",
      "type": "registry:component",
      "target": "components/team-challenge/lib/events.ts"
    },
    {
      "path": "src/registry/components/gamification/team-challenge/hooks/use-team-challenge.ts",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport type { TeamChallengeContextValue } from \"../types\";\n\nexport const TeamChallengeContext =\n  React.createContext<TeamChallengeContextValue | null>(null);\n\n/**\n * Read the challenge state from a surrounding `TeamChallengeRoot`.\n * For hand-assembled layouts; throws if used outside a `Root`.\n */\nexport function useTeamChallenge(): TeamChallengeContextValue {\n  const ctx = React.useContext(TeamChallengeContext);\n  if (ctx === null) {\n    throw new Error(\n      \"useTeamChallenge must be used within a <TeamChallengeRoot>.\",\n    );\n  }\n  return ctx;\n}\n",
      "type": "registry:component",
      "target": "components/team-challenge/hooks/use-team-challenge.ts"
    },
    {
      "path": "src/registry/components/gamification/team-challenge/parts/team-challenge-root.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nimport { useLatestRef } from \"../../_shared/gamification-kit\";\nimport { TeamChallengeContext } from \"../hooks/use-team-challenge\";\nimport {\n  DEFAULT_JOIN_LABEL,\n  DEFAULT_LEAVE_LABEL,\n  DEFAULT_NO_PENALTY_HINT,\n  deriveChallenge,\n} from \"../lib/derive\";\nimport { openedEvent, optInEvent } from \"../lib/events\";\nimport type {\n  TeamChallengeContextValue,\n  TeamChallengeRootProps,\n} from \"../types\";\n\n/**\n * Tier B — headless provider. The **single** source of derived state, the\n * **single** controlled-echo chokepoint (reads `challenge.optedIn`, holds no\n * local opt-in state — the card-tree/calendar controlled-echo lesson), and the\n * **single** telemetry chokepoint (`challenge.opened` mount-effect,\n * double-emit-guarded + `challenge.opt-in` on toggle). No layout opinion beyond\n * the labelled region + `reveal-up`; renders `children`.\n */\nexport function TeamChallengeRoot({\n  challenge,\n  team,\n  onOptInChange,\n  onEvent,\n  joinLabel = DEFAULT_JOIN_LABEL,\n  leaveLabel = DEFAULT_LEAVE_LABEL,\n  noPenaltyHint = DEFAULT_NO_PENALTY_HINT,\n  children,\n  className,\n  \"aria-label\": ariaLabel,\n}: TeamChallengeRootProps) {\n  // Latest props read through refs (shared `useLatestRef`, P3.5 / D-04) so\n  // `toggleOptIn` + the opened-effect stay stable and an inline\n  // `onEvent`/`onOptInChange` never re-subscribes anything.\n  const challengeRef = useLatestRef(challenge);\n  const teamRef = useLatestRef(team);\n  const onOptInChangeRef = useLatestRef(onOptInChange);\n  const onEventRef = useLatestRef(onEvent);\n\n  const derived = React.useMemo(() => deriveChallenge(challenge), [challenge]);\n\n  // `challenge.opened` — once on first reveal/mount (D-C5). Guard keyed on the\n  // challenge id: StrictMode double-invoke + re-renders don't re-fire, but a new\n  // challenge mounted into the same Root is correctly a fresh \"opened\".\n  const firedRef = React.useRef<string | null>(null);\n  React.useEffect(() => {\n    if (firedRef.current === challenge.id) return;\n    firedRef.current = challenge.id;\n    onEventRef.current?.(openedEvent(teamRef.current, challengeRef.current));\n    // `challengeRef`/`teamRef`/`onEventRef` are stable ref objects (identity\n    // never changes, only `.current`) — listed for exhaustive-deps, not\n    // because they trigger re-runs.\n  }, [challenge.id, challengeRef, onEventRef, teamRef]);\n\n  const toggleOptIn = React.useCallback(() => {\n    const next = !challengeRef.current.optedIn;\n    // Host callback first (it owns the value + re-renders with the new\n    // `challenge.optedIn`), then telemetry — the family's veto-friendly order.\n    onOptInChangeRef.current?.(next);\n    onEventRef.current?.(\n      optInEvent(teamRef.current, challengeRef.current, next),\n    );\n  }, [challengeRef, onEventRef, onOptInChangeRef, teamRef]);\n\n  const canOptIn = typeof onOptInChange === \"function\";\n\n  const contextValue = React.useMemo<TeamChallengeContextValue>(\n    () => ({\n      challenge,\n      team,\n      derived,\n      canOptIn,\n      toggleOptIn,\n      joinLabel,\n      leaveLabel,\n      noPenaltyHint,\n    }),\n    [\n      challenge,\n      team,\n      derived,\n      canOptIn,\n      toggleOptIn,\n      joinLabel,\n      leaveLabel,\n      noPenaltyHint,\n    ],\n  );\n\n  return (\n    <TeamChallengeContext.Provider value={contextValue}>\n      <div\n        role=\"group\"\n        aria-label={ariaLabel ?? challenge.label}\n        data-state={\n          derived.isComplete\n            ? \"complete\"\n            : derived.isActive\n              ? \"active\"\n              : \"joinable\"\n        }\n        className={cn(\n          \"reveal-up flex w-full flex-col gap-4 rounded-xl border bg-card p-5 text-card-foreground transition-colors\",\n          // Earned = signal-lime emphasis; active = subtle lime ring; joinable = neutral.\n          derived.isComplete\n            ? \"border-primary/60 bg-primary/5\"\n            : derived.isActive\n              ? \"border-primary/40\"\n              : \"border-border\",\n          className,\n        )}\n      >\n        {children}\n      </div>\n    </TeamChallengeContext.Provider>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/team-challenge/parts/team-challenge-root.tsx"
    },
    {
      "path": "src/registry/components/gamification/team-challenge/parts/team-challenge-header.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { CircleCheck } from \"lucide-react\";\n\nimport { Badge } from \"@/components/ui/badge\";\n\nimport { useTeamChallenge } from \"../hooks/use-team-challenge\";\nimport { TeamMemberStack } from \"./team-member-stack\";\n\n/**\n * Tier B — the card header: challenge `label` (truncated, full text on `title`)\n * + the state indicator + the optional team name + the `TeamMemberStack`.\n *\n * The state indicator makes opted-out read as *Optional* (an invitation), never\n * *failure* (§6.1). On `done` it becomes the **lightweight inline earned\n * acknowledgement** (§6.4): a \"Completed together\" pill (glyph + text, not color\n * alone) with a `.reveal-up` micro-entrance (globally reduced-motion-safe) and\n * `aria-live=\"polite\"` so the transition to done is announced once without\n * stealing focus or blocking — the heavy celebration is E6's job, never built\n * here (no modal, no overlay).\n */\nexport function TeamChallengeHeader({\n  showMemberStack = true,\n}: {\n  showMemberStack?: boolean;\n}) {\n  const { challenge, team, derived } = useTeamChallenge();\n\n  const stateIndicator = derived.isComplete ? (\n    // aria-live only on the earned pill → the transition to done is announced\n    // once (§6.4); Optional/Active are silent (a plain badge, no live region).\n    <span\n      aria-live=\"polite\"\n      className=\"reveal-up inline-flex items-center gap-1 rounded-4xl bg-primary px-2 py-0.5 text-xs font-medium text-primary-foreground\"\n    >\n      <CircleCheck aria-hidden className=\"size-3\" /> Completed together\n    </span>\n  ) : derived.isActive ? (\n    <Badge variant=\"secondary\">Active</Badge>\n  ) : (\n    <Badge variant=\"outline\">Optional</Badge>\n  );\n\n  return (\n    <div className=\"flex items-start justify-between gap-3\">\n      <div className=\"flex min-w-0 flex-col gap-1\">\n        <div className=\"flex items-center gap-2\">{stateIndicator}</div>\n        <h3\n          title={challenge.label}\n          className=\"truncate text-sm font-semibold text-foreground\"\n        >\n          {challenge.label}\n        </h3>\n        {team.name ? (\n          <p className=\"truncate text-xs text-muted-foreground\">{team.name}</p>\n        ) : null}\n      </div>\n      {showMemberStack ? (\n        <TeamMemberStack members={team.members} className=\"shrink-0 pt-0.5\" />\n      ) : null}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/team-challenge/parts/team-challenge-header.tsx"
    },
    {
      "path": "src/registry/components/gamification/team-challenge/parts/team-challenge-progress.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { Progress } from \"@/components/ui/progress\";\nimport { cn } from \"@/lib/utils\";\n\nimport { clamp } from \"../../_shared/gamification-kit\";\nimport { useTeamChallenge } from \"../hooks/use-team-challenge\";\nimport type { ChallengeProgressMeterProps } from \"../types\";\n\n// Built on the shared `clamp` (P3.5 / D-04) — same min/max shape as\n// `lib/derive.ts`'s `progressFraction`, now single-sourced.\nfunction clamp01(current: number, target: number): number {\n  return clamp(current / Math.max(target, 1), 0, 1);\n}\n\n/**\n * Tier C — dumb **collective** progress meter (system §6.3): ONE bar + ONE\n * `current / target` count (mono). There is deliberately no per-member row,\n * ranking, or \"you still need to commit\" framing — that would drift toward\n * per-individual tracking (excluded by design). The bar caps at 100%\n * (`clamp01`); the count shows the host's raw truth even past target. AT reads\n * the count via the accessible name (`aria-label` carries \"3 of 5\") — we pass\n * only `value` to the primitive, so this stays portable across the Radix ↔ Base\n * UI `Progress` divergence (Base UI has no `getValueLabel`).\n */\nexport function ChallengeProgressMeter({\n  current,\n  target,\n  complete = false,\n  className,\n  \"aria-label\": ariaLabel,\n}: ChallengeProgressMeterProps) {\n  const pct = Math.round(clamp01(current, target) * 100);\n\n  return (\n    <div className={cn(\"group flex flex-col gap-1.5\", className)}>\n      <Progress\n        value={pct}\n        aria-label={ariaLabel ?? `Team progress: ${current} of ${target}`}\n        className={cn(\n          // Hover: the lime fill brightens + the bar gains a soft lime glow.\n          \"h-2.5 bg-muted transition-shadow duration-200 group-hover:shadow-[0_0_10px] group-hover:shadow-primary/30\",\n          \"group-hover:**:data-[slot=progress-indicator]:brightness-110\",\n          \"motion-reduce:**:data-[slot=progress-indicator]:transition-none\",\n        )}\n      />\n      <span\n        className={cn(\n          \"font-mono text-xs tabular-nums\",\n          complete ? \"text-primary\" : \"text-muted-foreground\",\n        )}\n      >\n        {current} / {target}\n      </span>\n    </div>\n  );\n}\n\n/**\n * Tier B — context wrapper: reads the collective count from context and renders\n * the Tier-C meter. Never a per-member surface.\n */\nexport function TeamChallengeProgress() {\n  const { challenge, team, derived } = useTeamChallenge();\n  return (\n    <ChallengeProgressMeter\n      current={challenge.progress.current}\n      target={challenge.progress.target}\n      complete={derived.isComplete}\n      aria-label={`${team.name ?? \"Team\"} progress: ${derived.countLabel}`}\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "components/team-challenge/parts/team-challenge-progress.tsx"
    },
    {
      "path": "src/registry/components/gamification/team-challenge/parts/team-challenge-reward.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Gift } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nimport { useTeamChallenge } from \"../hooks/use-team-challenge\";\nimport type { ChallengeRewardChipProps } from \"../types\";\n\n/**\n * Tier C — dumb reward chip. Copy frames the reward as the team's **collectively**\n * (system D-08): *\"The team earns: …\"* (available) → *\"The team earned: …\"*\n * (`earned`). **Never** first-person (\"you'll get\") — a first-person framing\n * violates D-08, and there is no per-member reward path in the type system.\n * Token-based (signal-lime on earned via `--primary`), never an ad-hoc green.\n */\nexport function ChallengeRewardChip({\n  reward,\n  earned = false,\n  className,\n}: ChallengeRewardChipProps) {\n  return (\n    <div\n      className={cn(\n        \"flex items-center gap-2 rounded-lg border px-3 py-2 text-xs\",\n        earned\n          ? \"border-primary/40 bg-primary/10 text-foreground\"\n          : \"border-border bg-muted/50 text-muted-foreground\",\n        className,\n      )}\n    >\n      <Gift\n        aria-hidden\n        className={cn(\"size-4 shrink-0\", earned && \"text-primary\")}\n      />\n      <span className=\"min-w-0\">\n        <span className=\"font-medium text-foreground\">\n          {earned ? \"The team earned:\" : \"The team earns:\"}\n        </span>{\" \"}\n        <span className=\"break-words\">{reward}</span>\n      </span>\n    </div>\n  );\n}\n\n/**\n * Tier B — context wrapper: reads `reward` + `isComplete`; auto-hides when there\n * is no reward (§5.3). The assembly further gates it with `showReward`.\n */\nexport function TeamChallengeReward() {\n  const { challenge, derived } = useTeamChallenge();\n  if (!derived.hasReward || challenge.reward == null) return null;\n  return (\n    <ChallengeRewardChip reward={challenge.reward} earned={derived.isComplete} />\n  );\n}\n",
      "type": "registry:component",
      "target": "components/team-challenge/parts/team-challenge-reward.tsx"
    },
    {
      "path": "src/registry/components/gamification/team-challenge/parts/team-challenge-optin.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\n\nimport { useTeamChallenge } from \"../hooks/use-team-challenge\";\nimport {\n  DEFAULT_JOIN_LABEL,\n  DEFAULT_LEAVE_LABEL,\n  DEFAULT_NO_PENALTY_HINT,\n} from \"../lib/derive\";\nimport type { OptInToggleProps } from \"../types\";\n\n/**\n * Tier C — the context-free opt-in **button-toggle** (catalogue C5), droppable\n * on any surface. The never-forced rule made literal (system §5.2):\n *\n *  - **Join** (opted-out) is a *prominent, inviting* primary action — never a\n *    greyed/disabled/failure treatment.\n *  - **Leave** (opted-in) is a *plain, single-click* action — no confirm dialog,\n *    no \"are you sure you want to abandon?\" guilt, no warning tone. Leaving is\n *    as cost-free as joining.\n *  - The `noPenaltyHint` is visible in **both** states and associated with the\n *    control via `aria-describedby`, so cost-free is explicit in copy, not just\n *    behavior.\n *\n * A labelled button (the visible text carries the copy) reads never-forced far\n * better than a bare `switch` thumb (which implies a \"correct\" default). State\n * is also conveyed via `aria-pressed`.\n */\nexport function OptInToggle({\n  optedIn,\n  onOptInChange,\n  joinLabel = DEFAULT_JOIN_LABEL,\n  leaveLabel = DEFAULT_LEAVE_LABEL,\n  noPenaltyHint = DEFAULT_NO_PENALTY_HINT,\n  disabled = false,\n  className,\n}: OptInToggleProps) {\n  const hintId = React.useId();\n\n  return (\n    <div className={cn(\"flex flex-col gap-1.5\", className)}>\n      <Button\n        type=\"button\"\n        variant={optedIn ? \"outline\" : \"default\"}\n        size=\"sm\"\n        aria-pressed={optedIn}\n        aria-describedby={hintId}\n        disabled={disabled}\n        onClick={() => onOptInChange?.(!optedIn)}\n        className=\"w-fit\"\n      >\n        {optedIn ? leaveLabel : joinLabel}\n      </Button>\n      {noPenaltyHint ? (\n        <p id={hintId} className=\"text-xs text-muted-foreground\">\n          {noPenaltyHint}\n        </p>\n      ) : null}\n    </div>\n  );\n}\n\n/**\n * Tier B — context wrapper: the opt-in/opt-out control. **Hides entirely** when\n * no `onOptInChange` is wired (capability-gating → a read-only card falls out\n * for free, §6.2). The assembly further gates it with `showOptIn`.\n */\nexport function TeamChallengeOptIn() {\n  const { challenge, canOptIn, toggleOptIn, joinLabel, leaveLabel, noPenaltyHint } =\n    useTeamChallenge();\n\n  if (!canOptIn) return null;\n\n  return (\n    <OptInToggle\n      optedIn={challenge.optedIn}\n      onOptInChange={toggleOptIn}\n      joinLabel={joinLabel}\n      leaveLabel={leaveLabel}\n      noPenaltyHint={noPenaltyHint}\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "components/team-challenge/parts/team-challenge-optin.tsx"
    },
    {
      "path": "src/registry/components/gamification/team-challenge/parts/team-member-stack.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport {\n  Avatar,\n  AvatarFallback,\n  AvatarGroup,\n  AvatarGroupCount,\n  AvatarImage,\n} from \"@/components/ui/avatar\";\nimport { cn } from \"@/lib/utils\";\n\nimport { initialsFromName } from \"../../_shared/gamification-kit\";\nimport type { TeamMemberStackProps } from \"../types\";\n\n/**\n * Tier C — dumb avatar pile of `team.members`: image with an initials fallback,\n * `max` (default 5) then a `+N` overflow chip. **Identity only** — this is the\n * ONLY member-level surface (system §6.3); it reinforces *shared goal*, never a\n * per-member progress/contribution display (no per-avatar checkmark/state). The\n * pile is one accessible label (\"5 team members\"), not N announcements; the\n * avatars themselves are decorative (`aria-hidden`).\n */\nexport function TeamMemberStack({\n  members,\n  max = 5,\n  className,\n}: TeamMemberStackProps) {\n  if (members.length === 0) return null;\n\n  const shown = members.slice(0, max);\n  const overflow = members.length - shown.length;\n\n  return (\n    <div\n      role=\"group\"\n      aria-label={`${members.length} team ${members.length === 1 ? \"member\" : \"members\"}`}\n      className={cn(\"inline-flex\", className)}\n    >\n      <AvatarGroup aria-hidden>\n        {shown.map((member) => (\n          <Avatar key={member.id}>\n            {member.avatarUrl ? (\n              <AvatarImage src={member.avatarUrl} alt=\"\" />\n            ) : null}\n            <AvatarFallback>{initialsFromName(member.displayName)}</AvatarFallback>\n          </Avatar>\n        ))}\n        {overflow > 0 ? <AvatarGroupCount>+{overflow}</AvatarGroupCount> : null}\n      </AvatarGroup>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/team-challenge/parts/team-member-stack.tsx"
    },
    {
      "path": "src/registry/components/gamification/team-challenge/parts/team-challenge-skeleton.tsx",
      "content": "import * as React from \"react\";\n\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * Tier C — the loading/empty state as a **component** (not a `loading` prop —\n * gantt precedent): a card silhouette with a meter shimmer, composable anywhere\n * while the host fetches. Purely presentational, context-free, framework-free.\n */\nexport function TeamChallengeSkeleton({\n  className,\n}: {\n  className?: string;\n}) {\n  return (\n    <div\n      aria-hidden\n      className={cn(\n        \"flex w-full flex-col gap-4 rounded-xl border border-border bg-card p-5\",\n        className,\n      )}\n    >\n      <div className=\"flex items-start justify-between gap-3\">\n        <div className=\"flex flex-col gap-2\">\n          <Skeleton className=\"h-5 w-16 rounded-4xl\" />\n          <Skeleton className=\"h-4 w-48\" />\n          <Skeleton className=\"h-3 w-24\" />\n        </div>\n        <div className=\"flex -space-x-2\">\n          <Skeleton className=\"size-8 rounded-full\" />\n          <Skeleton className=\"size-8 rounded-full\" />\n          <Skeleton className=\"size-8 rounded-full\" />\n        </div>\n      </div>\n      <div className=\"flex flex-col gap-1.5\">\n        <Skeleton className=\"h-2.5 w-full rounded-full\" />\n        <Skeleton className=\"h-3 w-12\" />\n      </div>\n      <Skeleton className=\"h-9 w-32 rounded-md\" />\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/team-challenge/parts/team-challenge-skeleton.tsx"
    }
  ],
  "categories": [
    "gamification"
  ],
  "type": "registry:block"
}