{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "team-feedback-loop",
  "title": "Team Feedback Loop",
  "author": "ilinxa",
  "description": "Non-blocking celebration layer — a brief skippable overlay when team progress advances, plus a gentle dismissible next-task nudge.",
  "dependencies": [
    "canvas-confetti",
    "lucide-react"
  ],
  "devDependencies": [
    "@types/canvas-confetti"
  ],
  "registryDependencies": [
    "button",
    "@ilinxa/gamification-kit"
  ],
  "files": [
    {
      "path": "src/registry/components/gamification/team-feedback-loop/team-feedback-loop.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { TeamFeedbackCelebration } from \"./parts/team-feedback-celebration\";\nimport { TeamFeedbackLoopRoot } from \"./parts/team-feedback-loop-root\";\nimport { TeamFeedbackNudge } from \"./parts/team-feedback-nudge\";\nimport type { TeamFeedbackLoopProps, TeamFeedbackLoopHandle } from \"./types\";\n\n/**\n * Tier A — the batteries-included assembly: `Root` + `showCelebration`-gated\n * celebration + `showNudge`-gated nudge. Forwards its `ref` straight to `Root`\n * (which owns the imperative handle). Contains **no logic the parts don't** — a\n * hand-assembled layout behaves identically.\n *\n * A host-triggered, NON-BLOCKING (D-10) cooperative feedback layer: a brief (< 1s),\n * skippable celebration + a gentle next-task nudge. Owns no milestone/badge/task\n * data. Team-scoped copy only — never an individual (D-08).\n */\nexport const TeamFeedbackLoop = React.forwardRef<\n  TeamFeedbackLoopHandle,\n  TeamFeedbackLoopProps\n>(function TeamFeedbackLoop(\n  { showCelebration = true, showNudge = true, ...rootProps },\n  ref,\n) {\n  return (\n    <TeamFeedbackLoopRoot ref={ref} {...rootProps}>\n      {showCelebration ? <TeamFeedbackCelebration /> : null}\n      {showNudge ? <TeamFeedbackNudge /> : null}\n    </TeamFeedbackLoopRoot>\n  );\n});\n",
      "type": "registry:component",
      "target": "components/team-feedback-loop/team-feedback-loop.tsx"
    },
    {
      "path": "src/registry/components/gamification/team-feedback-loop/index.ts",
      "content": "// Tier A — batteries-included assembly.\nexport { TeamFeedbackLoop } from \"./team-feedback-loop\";\n\n// Tier B — headless provider + flat à-la-carte parts.\nexport { TeamFeedbackLoopRoot } from \"./parts/team-feedback-loop-root\";\nexport { TeamFeedbackCelebration } from \"./parts/team-feedback-celebration\";\nexport { TeamFeedbackNudge } from \"./parts/team-feedback-nudge\";\n\n// Tier C — context-free primitives. ConfettiBurst is the lazy-chunk host; the\n// Grid pulls it via React.lazy, so importing it here for standalone use does not\n// force it into a default consumer's bundle (unused re-exports tree-shake).\nexport { CelebrationOverlay } from \"./parts/celebration-overlay\";\nexport { NextTaskNudge } from \"./parts/next-task-nudge\";\nexport { default as ConfettiBurst } from \"./parts/confetti-burst\";\n\n// Context consumer for hand-assembled layouts.\nexport { useTeamFeedbackLoop } from \"./hooks/use-team-feedback-context\";\n\n// Public types.\nexport type {\n  TeamFeedbackLoopProps,\n  TeamFeedbackLoopRootProps,\n  TeamFeedbackLoopBaseProps,\n  TeamFeedbackLoopHandle,\n  TeamFeedbackContextValue,\n  CelebrationOverlayProps,\n  NextTaskNudgeProps,\n  FeedbackEvent,\n  NextTaskSuggestion,\n  NudgePlacement,\n  GamificationEvent,\n} from \"./types\";\n",
      "type": "registry:component",
      "target": "components/team-feedback-loop/index.ts"
    },
    {
      "path": "src/registry/components/gamification/team-feedback-loop/types.ts",
      "content": "import type { ReactNode } from \"react\";\n\n/**\n * `team-feedback-loop` — 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 is **re-declared locally** — this\n * component imports nothing from another registry component. These E6 trigger\n * shapes are intentionally component-local (system §4), not in the shared model.\n */\n\n/** What just happened — drives the celebration. Host-owned; pushed in. */\nexport interface FeedbackEvent {\n  kind: \"milestone\" | \"badge\" | \"task-complete\";\n  /** Team-scoped copy (\"Your team…\"); NEVER an individual (D-08). */\n  title: string;\n  detail?: string;\n  /** Optional progression-loop chapter line (E5 overlap). */\n  narrativeBeat?: string;\n  /**\n   * Optional explicit re-trigger key (v0.1.2, additive). The controlled\n   * `event` prop is compared by CONTENT (`kind` + `title` + `detail` +\n   * `narrativeBeat` + `eventKey`), NOT by object identity — an inline object\n   * literal with unchanged content does not re-open the celebration on parent\n   * re-renders, and a dismissed celebration stays dismissed. Change this key\n   * (e.g. a counter or the source record's id) to force a re-open when the\n   * SAME content should legitimately celebrate again.\n   */\n  eventKey?: string | number;\n}\n\n/** What to do next — drives the nudge. Closes the engagement loop. */\nexport interface NextTaskSuggestion {\n  /** Opaque to this component; returned on accept. */\n  taskId: string;\n  label: string;\n}\n\n/**\n * Telemetry slice (D-07 symmetry). **This component emits NOTHING** — the\n * catalogue lists no telemetry event for E6 (C4: accept-but-silent). The prop is\n * accepted for API symmetry; the host types its own envelope.\n */\nexport type GamificationEvent = { type: string; teamId: string; [key: string]: unknown };\n\nexport type NudgePlacement = \"inline\" | \"corner\";\n\n/** Data + trigger + callback surface shared by the assembly and the headless `Root`. */\nexport interface TeamFeedbackLoopBaseProps {\n  /** Team identity for team-scoped copy + (if wired) telemetry envelope. Identity-only (D-15). */\n  teamId: string;\n\n  // Celebration — controlled trigger (primary, C3).\n  /** Set → (re)open the celebration; `null` → close (silently). Omit entirely for imperative-only. */\n  event?: FeedbackEvent | null;\n  /** Auto-dismiss after N ms. Clamped to [200, 999) (D-10). Default ~800. */\n  celebrationDurationMs?: number;\n  /** Opt-in lazy confetti for `milestone`/`badge` kinds. Default `false`. */\n  enableConfetti?: boolean;\n  /** Override the celebration body; the wrapper stays non-blocking regardless. */\n  renderCelebration?: (event: FeedbackEvent) => ReactNode;\n  /** Fired when the celebration is dismissed by the timer (`auto`) or the user (`skip`). */\n  onCelebrationDismiss?: (event: FeedbackEvent, reason: \"auto\" | \"skip\") => void;\n\n  // Next-task nudge.\n  nextTask?: NextTaskSuggestion;\n  onNextTask?: (suggestion: NextTaskSuggestion) => void;\n  onNudgeDismiss?: (suggestion: NextTaskSuggestion) => void;\n  /** Where the nudge mounts. Default `\"inline\"`. NEVER modal. */\n  nudgePlacement?: NudgePlacement;\n\n  /** Telemetry (D-07) — accepted for symmetry, emits nothing for E6 (C4). */\n  onEvent?: (event: GamificationEvent) => void;\n\n  className?: string;\n  \"aria-label\"?: string;\n}\n\n/** Props for the batteries-included `TeamFeedbackLoop` assembly. */\nexport interface TeamFeedbackLoopProps extends TeamFeedbackLoopBaseProps {\n  /** Render the celebration surface. Default `true`. */\n  showCelebration?: boolean;\n  /** Render the nudge surface. Default `true`. */\n  showNudge?: boolean;\n}\n\n/** Props for the headless provider `TeamFeedbackLoopRoot`. */\nexport interface TeamFeedbackLoopRootProps extends TeamFeedbackLoopBaseProps {\n  children: ReactNode;\n}\n\n/** Imperative handle (C3 — both trigger paths, controlled-primary). Lives on `Root`. */\nexport interface TeamFeedbackLoopHandle {\n  /** Fire a celebration imperatively → reducer `open`. */\n  celebrate(event: FeedbackEvent): void;\n  /** Dismiss the current celebration now → reducer `close` (reason `\"skip\"`). */\n  dismiss(): void;\n}\n\n/** Props for the context-free Tier-C `CelebrationOverlay`. */\nexport interface CelebrationOverlayProps {\n  event: FeedbackEvent;\n  reducedMotion: boolean;\n  onSkip: () => void;\n  render?: (event: FeedbackEvent) => ReactNode;\n  /** Confetti burst slot (rendered inside the non-blocking wrapper). */\n  children?: ReactNode;\n  className?: string;\n}\n\n/** Props for the context-free Tier-C `NextTaskNudge`. */\nexport interface NextTaskNudgeProps {\n  suggestion: NextTaskSuggestion;\n  placement?: NudgePlacement;\n  onAccept: (suggestion: NextTaskSuggestion) => void;\n  onDismiss: (suggestion: NextTaskSuggestion) => void;\n  className?: string;\n}\n\n/** The resolved state every context part reads — one source of truth. */\nexport interface TeamFeedbackContextValue {\n  current: FeedbackEvent | null;\n  reducedMotion: boolean;\n  enableConfetti: boolean;\n  renderCelebration?: (event: FeedbackEvent) => ReactNode;\n  /** Dismiss the celebration now (fires `onCelebrationDismiss(event, \"skip\")`). */\n  skip: () => void;\n  /** The visible nudge (null when unset or dismissed). */\n  nudge: NextTaskSuggestion | null;\n  nudgePlacement: NudgePlacement;\n  acceptNudge: (suggestion: NextTaskSuggestion) => void;\n  dismissNudge: (suggestion: NextTaskSuggestion) => void;\n  teamId: string;\n}\n",
      "type": "registry:component",
      "target": "components/team-feedback-loop/types.ts"
    },
    {
      "path": "src/registry/components/gamification/team-feedback-loop/lib/clamp.ts",
      "content": "/**\n * Pure duration clamp — the D-10 non-blocking guarantee in code. Even if a\n * consumer passes `celebrationDurationMs={5000}`, the celebration is clamped to\n * < 1000 ms; a sub-200 ms flash (unreadable) is floored to 200 ms. Framework-free,\n * unit-testable, kit-extraction candidate.\n */\n\nexport const DEFAULT_CELEBRATION_MS = 800;\nexport const MIN_CELEBRATION_MS = 200;\n/** Hard ceiling — strictly < 1000 ms (D-10). */\nexport const MAX_CELEBRATION_MS = 999;\n\nexport function clampDuration(ms?: number): number {\n  return Math.min(\n    Math.max(ms ?? DEFAULT_CELEBRATION_MS, MIN_CELEBRATION_MS),\n    MAX_CELEBRATION_MS,\n  );\n}\n",
      "type": "registry:component",
      "target": "components/team-feedback-loop/lib/clamp.ts"
    },
    {
      "path": "src/registry/components/gamification/team-feedback-loop/hooks/use-team-feedback-context.ts",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport type { TeamFeedbackContextValue } from \"../types\";\n\nexport const TeamFeedbackContext =\n  React.createContext<TeamFeedbackContextValue | null>(null);\n\n/**\n * Read the current feedback state from a surrounding `TeamFeedbackLoopRoot`.\n * For hand-assembled layouts; throws if used outside a `Root`.\n */\nexport function useTeamFeedbackLoop(): TeamFeedbackContextValue {\n  const ctx = React.useContext(TeamFeedbackContext);\n  if (ctx === null) {\n    throw new Error(\n      \"useTeamFeedbackLoop must be used within a <TeamFeedbackLoopRoot>.\",\n    );\n  }\n  return ctx;\n}\n",
      "type": "registry:component",
      "target": "components/team-feedback-loop/hooks/use-team-feedback-context.ts"
    },
    {
      "path": "src/registry/components/gamification/team-feedback-loop/parts/team-feedback-loop-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 { TeamFeedbackContext } from \"../hooks/use-team-feedback-context\";\nimport { clampDuration } from \"../lib/clamp\";\nimport type {\n  FeedbackEvent,\n  NextTaskSuggestion,\n  TeamFeedbackContextValue,\n  TeamFeedbackLoopHandle,\n  TeamFeedbackLoopRootProps,\n} from \"../types\";\n\n// --- reduced-motion as an external store (SSR-safe; no set-state-in-effect) ---\nfunction subscribeReducedMotion(callback: () => void): () => void {\n  if (typeof window === \"undefined\" || !window.matchMedia) return () => {};\n  const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n  mq.addEventListener(\"change\", callback);\n  return () => mq.removeEventListener(\"change\", callback);\n}\nfunction getReducedMotionSnapshot(): boolean {\n  if (typeof window === \"undefined\" || !window.matchMedia) return false;\n  return window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n}\nfunction getReducedMotionServerSnapshot(): boolean {\n  return false;\n}\n\n// --- controlled-event content equality (v0.1.2, review 5.12) ---\n// The controlled `event` prop is diffed by CONTENT so an inline object literal\n// re-created each parent render doesn't re-dispatch `open`. `eventKey` is the\n// explicit escape hatch: bump it to force a re-open with identical copy.\nfunction sameEventContent(\n  a: FeedbackEvent | null,\n  b: FeedbackEvent | null,\n): boolean {\n  if (a === b) return true;\n  if (a === null || b === null) return false;\n  return (\n    a.kind === b.kind &&\n    a.title === b.title &&\n    a.detail === b.detail &&\n    a.narrativeBeat === b.narrativeBeat &&\n    a.eventKey === b.eventKey\n  );\n}\n\n// --- the single chokepoint reducer (both trigger paths funnel here, C3/C5) ---\ntype FeedbackState = {\n  current: FeedbackEvent | null;\n  epoch: number;\n  dismissedTaskId: string | null;\n};\n\ntype FeedbackAction =\n  | { type: \"open\"; event: FeedbackEvent }\n  | { type: \"close\" }\n  | { type: \"nudge-dismiss\"; taskId: string }\n  | { type: \"nudge-rearm\" };\n\nfunction reducer(state: FeedbackState, action: FeedbackAction): FeedbackState {\n  switch (action.type) {\n    case \"open\":\n      // Newest wins, never stack: one slot; bump epoch to re-arm the timer (C5).\n      return { ...state, current: action.event, epoch: state.epoch + 1 };\n    case \"close\":\n      return state.current === null ? state : { ...state, current: null };\n    case \"nudge-dismiss\":\n      return { ...state, dismissedTaskId: action.taskId };\n    case \"nudge-rearm\":\n      return state.dismissedTaskId === null\n        ? state\n        : { ...state, dismissedTaskId: null };\n    default:\n      return state;\n  }\n}\n\n/**\n * Tier B — headless provider. Owns the single current-event reducer that BOTH\n * the controlled `event` prop AND the imperative `celebrate()`/`dismiss()` handle\n * funnel into (C3), the < 1s auto-dismiss timer (clamped, D-10), the reduced-motion\n * read, the skip/Esc handlers, and the nudge dismissed/re-arm state. Holds NO\n * milestone/badge/task data — only \"what to show right now\". `onCelebrationDismiss`\n * fires ONLY on auto/skip/imperative-dismiss, never on a host-driven `event = null`\n * (controlled-echo). SSR-safe: `current` starts null, `reducedMotion` false on the\n * server → no animation/timer on first paint.\n */\nexport const TeamFeedbackLoopRoot = React.forwardRef<\n  TeamFeedbackLoopHandle,\n  TeamFeedbackLoopRootProps\n>(function TeamFeedbackLoopRoot(\n  {\n    teamId,\n    event,\n    celebrationDurationMs,\n    enableConfetti = false,\n    renderCelebration,\n    onCelebrationDismiss,\n    nextTask,\n    onNextTask,\n    onNudgeDismiss,\n    nudgePlacement = \"inline\",\n    className,\n    \"aria-label\": ariaLabel,\n    children,\n  },\n  ref,\n) {\n  const [state, dispatch] = React.useReducer(reducer, {\n    current: null,\n    epoch: 0,\n    dismissedTaskId: null,\n  });\n  // Destructure to locals so effect/memo deps are plain values (the exhaustive-deps\n  // rule can't verify reducer-state member access is immutable).\n  const { current, epoch, dismissedTaskId } = state;\n\n  const reducedMotion = React.useSyncExternalStore(\n    subscribeReducedMotion,\n    getReducedMotionSnapshot,\n    getReducedMotionServerSnapshot,\n  );\n\n  // --- stable callback refs (P3.5 / D-04: shared `useLatestRef`, assigned in\n  // an effect, never during render — not a `setState`) ---\n  const onDismissRef = useLatestRef(onCelebrationDismiss);\n  const onAcceptRef = useLatestRef(onNextTask);\n  const onNudgeDismissRef = useLatestRef(onNudgeDismiss);\n  const currentRef = useLatestRef(current);\n\n  // --- skip = user/imperative dismiss (fires the callback with reason \"skip\") ---\n  const skip = React.useCallback(() => {\n    const evt = currentRef.current;\n    if (evt) onDismissRef.current?.(evt, \"skip\");\n    dispatch({ type: \"close\" });\n    // `currentRef`/`onDismissRef` are stable ref objects — listed for\n    // exhaustive-deps only, never trigger a re-create.\n  }, [currentRef, onDismissRef]);\n\n  // --- auto-dismiss timer: armed per open (keyed on current + epoch), cleared on close ---\n  React.useEffect(() => {\n    if (current === null) return;\n    const ms = clampDuration(celebrationDurationMs);\n    const id = window.setTimeout(() => {\n      onDismissRef.current?.(current, \"auto\");\n      dispatch({ type: \"close\" });\n    }, ms);\n    return () => window.clearTimeout(id);\n  }, [current, epoch, celebrationDurationMs, onDismissRef]);\n\n  // --- controlled `event` prop → funnels into the reducer (undefined = imperative-only) ---\n  // v0.1.2 (review 5.12): compared by CONTENT, not identity. An inline object\n  // literal — the idiomatic React shape — used to re-dispatch `open` on every\n  // parent render: the auto-dismiss timer re-armed forever and a skipped\n  // celebration re-opened. Same content ⇒ no re-open; a changed `eventKey`\n  // (see types.ts) is part of the content and force-re-opens on demand.\n  const lastEventRef = React.useRef<FeedbackEvent | null | undefined>(undefined);\n  React.useEffect(() => {\n    if (event === undefined) return; // uncontrolled: imperative handle only\n    const last = lastEventRef.current;\n    if (last !== undefined && sameEventContent(last, event)) {\n      lastEventRef.current = event; // track the latest reference, no dispatch\n      return;\n    }\n    lastEventRef.current = event;\n    if (event != null) {\n      dispatch({ type: \"open\", event });\n    } else {\n      // Host-driven close — silent (no onCelebrationDismiss echo).\n      dispatch({ type: \"close\" });\n    }\n  }, [event]);\n\n  // --- Esc skips (additive, non-trapping) while a celebration is open ---\n  React.useEffect(() => {\n    if (current === null) return;\n    const onKeyDown = (e: KeyboardEvent) => {\n      if (e.key === \"Escape\") skip();\n    };\n    document.addEventListener(\"keydown\", onKeyDown);\n    return () => document.removeEventListener(\"keydown\", onKeyDown);\n  }, [current, skip]);\n\n  // --- imperative handle (both paths land in the same reducer) ---\n  React.useImperativeHandle(\n    ref,\n    () => ({\n      celebrate(evt: FeedbackEvent) {\n        dispatch({ type: \"open\", event: evt });\n      },\n      dismiss() {\n        skip();\n      },\n    }),\n    [skip],\n  );\n\n  // --- nudge: re-arm when a NEW taskId arrives ---\n  const lastTaskIdRef = React.useRef<string | undefined>(undefined);\n  React.useEffect(() => {\n    const taskId = nextTask?.taskId;\n    if (taskId === lastTaskIdRef.current) return;\n    lastTaskIdRef.current = taskId;\n    if (taskId != null) dispatch({ type: \"nudge-rearm\" });\n  }, [nextTask?.taskId]);\n\n  const acceptNudge = React.useCallback(\n    (s: NextTaskSuggestion) => {\n      onAcceptRef.current?.(s);\n    },\n    [onAcceptRef],\n  );\n  const dismissNudge = React.useCallback(\n    (s: NextTaskSuggestion) => {\n      dispatch({ type: \"nudge-dismiss\", taskId: s.taskId });\n      onNudgeDismissRef.current?.(s);\n    },\n    [onNudgeDismissRef],\n  );\n\n  const nudge =\n    nextTask && nextTask.taskId !== dismissedTaskId ? nextTask : null;\n\n  const contextValue = React.useMemo<TeamFeedbackContextValue>(\n    () => ({\n      current,\n      reducedMotion,\n      enableConfetti,\n      renderCelebration,\n      skip,\n      nudge,\n      nudgePlacement,\n      acceptNudge,\n      dismissNudge,\n      teamId,\n    }),\n    [\n      current,\n      reducedMotion,\n      enableConfetti,\n      renderCelebration,\n      skip,\n      nudge,\n      nudgePlacement,\n      acceptNudge,\n      dismissNudge,\n      teamId,\n    ],\n  );\n\n  return (\n    <TeamFeedbackContext.Provider value={contextValue}>\n      <div\n        data-slot=\"team-feedback-loop\"\n        className={cn(className)}\n        aria-label={ariaLabel}\n      >\n        {children}\n      </div>\n    </TeamFeedbackContext.Provider>\n  );\n});\n",
      "type": "registry:component",
      "target": "components/team-feedback-loop/parts/team-feedback-loop-root.tsx"
    },
    {
      "path": "src/registry/components/gamification/team-feedback-loop/parts/team-feedback-celebration.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { useTeamFeedbackLoop } from \"../hooks/use-team-feedback-context\";\nimport { CelebrationOverlay } from \"./celebration-overlay\";\n\n// The confetti value enters the graph ONLY here, via React.lazy — the default\n// CSS flourish and `enableConfetti={false}` never load this chunk.\nconst ConfettiBurst = React.lazy(() => import(\"./confetti-burst\"));\n\n/**\n * Tier B — reads context and renders the current `FeedbackEvent` as a\n * `CelebrationOverlay`. Gates the lazy `ConfettiBurst` on\n * `enableConfetti && !reducedMotion && kind ∈ {milestone, badge}`, and preloads\n * its chunk at open time (so the burst is usually ready within the < 1s window;\n * if not, it's simply skipped — the CSS flourish already played). Renders nothing\n * when no event is current.\n */\nexport function TeamFeedbackCelebration() {\n  const { current, reducedMotion, enableConfetti, renderCelebration, skip } =\n    useTeamFeedbackLoop();\n\n  const confettiEligible =\n    enableConfetti &&\n    !reducedMotion &&\n    current != null &&\n    (current.kind === \"milestone\" || current.kind === \"badge\");\n\n  // Preload-on-arm: kick the import when an eligible event opens.\n  React.useEffect(() => {\n    if (confettiEligible) {\n      void import(\"./confetti-burst\").catch(() => {});\n    }\n  }, [confettiEligible]);\n\n  if (current === null) return null;\n\n  return (\n    <CelebrationOverlay\n      event={current}\n      reducedMotion={reducedMotion}\n      onSkip={skip}\n      render={renderCelebration}\n    >\n      {confettiEligible ? (\n        <React.Suspense fallback={null}>\n          <ConfettiBurst />\n        </React.Suspense>\n      ) : null}\n    </CelebrationOverlay>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/team-feedback-loop/parts/team-feedback-celebration.tsx"
    },
    {
      "path": "src/registry/components/gamification/team-feedback-loop/parts/team-feedback-nudge.tsx",
      "content": "\"use client\";\n\nimport { useTeamFeedbackLoop } from \"../hooks/use-team-feedback-context\";\nimport { NextTaskNudge } from \"./next-task-nudge\";\n\n/**\n * Tier B — reads context and renders the current nudge (suppressed while\n * dismissed). Independent of the celebration's < 1s lifecycle (C6) — a standing,\n * dismissible prompt. Renders nothing when there's no active suggestion.\n */\nexport function TeamFeedbackNudge() {\n  const { nudge, nudgePlacement, acceptNudge, dismissNudge } =\n    useTeamFeedbackLoop();\n\n  if (nudge === null) return null;\n\n  return (\n    <NextTaskNudge\n      suggestion={nudge}\n      placement={nudgePlacement}\n      onAccept={acceptNudge}\n      onDismiss={dismissNudge}\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "components/team-feedback-loop/parts/team-feedback-nudge.tsx"
    },
    {
      "path": "src/registry/components/gamification/team-feedback-loop/parts/celebration-overlay.tsx",
      "content": "\"use client\";\n\nimport { CheckCircle2, PartyPopper, Trophy, X } from \"lucide-react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\n\nimport type { CelebrationOverlayProps, FeedbackEvent } from \"../types\";\n\nconst KIND_ICON: Record<FeedbackEvent[\"kind\"], typeof PartyPopper> = {\n  milestone: PartyPopper,\n  badge: Trophy,\n  \"task-complete\": CheckCircle2,\n};\n\n/**\n * Tier C — dumb, prop-driven celebration flourish. **The D-10 non-blocking\n * contract lives here:** the fixed wrapper AND the card are `pointer-events: none`\n * so clicks pass straight through to the board behind; the ONLY `pointer-events:\n * auto` element is the skip button. It never moves/traps focus, never renders a\n * work-halting scrim, and is a bottom-edge band, never a centered modal. Announces\n * via `role=\"status\"` + `aria-live=\"polite\"` (non-interrupting). Under reduced\n * motion the `reveal-up` entrance is dropped (static, instantly readable).\n * The `children` slot hosts the (already reduced-motion-gated) confetti burst.\n */\nexport function CelebrationOverlay({\n  event,\n  reducedMotion,\n  onSkip,\n  render,\n  children,\n  className,\n}: CelebrationOverlayProps) {\n  const Icon = KIND_ICON[event.kind];\n\n  return (\n    <div\n      role=\"status\"\n      aria-live=\"polite\"\n      className={cn(\n        \"pointer-events-none fixed inset-x-0 bottom-6 z-50 flex justify-center px-4\",\n        className,\n      )}\n    >\n      {children}\n      <div\n        className={cn(\n          \"pointer-events-none flex max-w-md items-start gap-3 rounded-xl border border-border bg-card px-4 py-3 text-card-foreground shadow-lg\",\n          !reducedMotion && \"reveal-up\",\n        )}\n      >\n        {render ? (\n          render(event)\n        ) : (\n          <>\n            <span className=\"flex size-9 shrink-0 items-center justify-center rounded-lg bg-primary text-primary-foreground\">\n              <Icon className=\"size-5\" aria-hidden />\n            </span>\n            <div className=\"flex min-w-0 flex-col gap-0.5\">\n              <p className=\"line-clamp-2 text-sm font-semibold text-foreground\">\n                {event.title}\n              </p>\n              {event.detail ? (\n                <p className=\"line-clamp-2 text-xs text-muted-foreground\">\n                  {event.detail}\n                </p>\n              ) : null}\n              {event.narrativeBeat ? (\n                <p className=\"mt-1 font-mono text-[0.7rem] uppercase tracking-wide text-primary/80\">\n                  {event.narrativeBeat}\n                </p>\n              ) : null}\n            </div>\n          </>\n        )}\n        <Button\n          variant=\"ghost\"\n          size=\"icon-sm\"\n          onClick={onSkip}\n          aria-label=\"Dismiss celebration\"\n          className=\"pointer-events-auto shrink-0\"\n        >\n          <X className=\"size-4\" aria-hidden />\n        </Button>\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/team-feedback-loop/parts/celebration-overlay.tsx"
    },
    {
      "path": "src/registry/components/gamification/team-feedback-loop/parts/next-task-nudge.tsx",
      "content": "\"use client\";\n\nimport { ArrowRight, X } from \"lucide-react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\n\nimport type { NextTaskNudgeProps } from \"../types\";\n\n/**\n * Tier C — dumb, prop-driven next-task nudge. A gentle, standing, dismissible\n * prompt — NEVER a modal. Accept + dismiss are real keyboard-operable `button`s;\n * dismiss is penalty-free (D-08). Long labels line-clamp with a `title` fallback;\n * corner placement caps its width so it never covers primary board actions.\n */\nexport function NextTaskNudge({\n  suggestion,\n  placement = \"inline\",\n  onAccept,\n  onDismiss,\n  className,\n}: NextTaskNudgeProps) {\n  return (\n    <div\n      role=\"group\"\n      aria-label=\"Suggested next task\"\n      className={cn(\n        \"flex items-center gap-3 rounded-lg border border-border bg-card px-3 py-2 text-card-foreground\",\n        placement === \"corner\" &&\n          \"fixed bottom-4 right-4 z-40 max-w-xs shadow-lg\",\n        className,\n      )}\n    >\n      <span className=\"flex size-7 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground\">\n        <ArrowRight className=\"size-4\" aria-hidden />\n      </span>\n      <div className=\"flex min-w-0 flex-col\">\n        <span className=\"text-[0.7rem] font-medium uppercase tracking-wide text-muted-foreground\">\n          Next up\n        </span>\n        <span\n          className=\"line-clamp-2 text-sm text-foreground\"\n          title={suggestion.label}\n        >\n          {suggestion.label}\n        </span>\n      </div>\n      <div className=\"ml-auto flex shrink-0 items-center gap-1\">\n        <Button size=\"sm\" variant=\"secondary\" onClick={() => onAccept(suggestion)}>\n          Start\n        </Button>\n        <Button\n          size=\"icon-sm\"\n          variant=\"ghost\"\n          onClick={() => onDismiss(suggestion)}\n          aria-label=\"Dismiss suggestion\"\n        >\n          <X className=\"size-4\" aria-hidden />\n        </Button>\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/team-feedback-loop/parts/next-task-nudge.tsx"
    },
    {
      "path": "src/registry/components/gamification/team-feedback-loop/parts/confetti-burst.tsx",
      "content": "\"use client\";\n\n// Dep-declaring import FIRST (the validate:meta-deps regex stops at the first\n// `from`-import). This is the ONLY module that imports the confetti value — it is\n// pulled solely via `React.lazy(() => import(\"./confetti-burst\"))`, so the default\n// CSS flourish and `enableConfetti={false}` never load its chunk.\nimport confetti from \"canvas-confetti\";\nimport * as React from \"react\";\n\n/**\n * Tier C (lazy) — a one-shot, non-blocking confetti burst on mount. `canvas-confetti`\n * injects its own fixed, `pointer-events: none` canvas (inherently non-blocking, D-10)\n * and self-cleans. This component is never mounted under `prefers-reduced-motion`\n * (the celebration part gates it), and `disableForReducedMotion` is set as a\n * belt-and-suspenders. Renders no DOM. **DEFAULT export** for `React.lazy`.\n */\nexport default function ConfettiBurst() {\n  React.useEffect(() => {\n    // Signal-lime-forward palette (a canvas needs raw hex; not a UI token surface).\n    confetti({\n      particleCount: 90,\n      spread: 72,\n      startVelocity: 38,\n      scalar: 0.9,\n      origin: { y: 0.75 },\n      colors: [\"#c3f53c\", \"#a3e635\", \"#84cc16\", \"#ffffff\"],\n      disableForReducedMotion: true,\n    });\n  }, []);\n\n  return null;\n}\n",
      "type": "registry:component",
      "target": "components/team-feedback-loop/parts/confetti-burst.tsx"
    }
  ],
  "categories": [
    "gamification"
  ],
  "type": "registry:block"
}