{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "flashcard-study-deck",
  "title": "Flashcard Study Deck",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-angular/blocks/flashcard-study-deck/flashcard-study-deck.component.ts",
      "content": "// Anki and Quizlet style spaced-repetition flashcard study deck with smooth 3D flip card\n// interaction, WCAG 2.2 accessibility curriculum, hint drawer, mastery percentage tracking,\n// keyboard shortcuts, and SM-2 interval rating buttons. Port of the React block 1:1.\nimport {\n  ChangeDetectionStrategy,\n  Component,\n  DestroyRef,\n  Input,\n  OnDestroy,\n  OnInit,\n  computed,\n  inject,\n  signal,\n} from '@angular/core'\nimport {\n  Bookmark,\n  Check,\n  ChevronLeft,\n  ChevronRight,\n  Copy,\n  Flame,\n  GraduationCap,\n  Lightbulb,\n  LucideAngularModule,\n  RotateCw,\n  Shuffle,\n} from 'lucide-angular'\nimport { cn } from '@/lib/utils'\nimport { UiBadgeComponent } from '@/ui/badge/badge.component'\nimport { UiButtonComponent } from '@/ui/button/button.component'\nimport { UiCardComponent } from '@/ui/card/card.component'\nimport { UiProgressComponent } from '@/ui/progress/progress.component'\nimport { UiSeparatorComponent } from '@/ui/separator/separator.component'\n\nexport interface Flashcard {\n  id: string\n  category: string\n  difficulty?: 'Level A' | 'Level AA' | 'Level AAA' | 'Design System' | 'Engineering'\n  question: string\n  hint: string\n  answer: string\n  codeSnippet?: string\n  codeLanguage?: string\n  source?: string\n}\n\nexport type RatingInterval = 'again' | 'hard' | 'good' | 'easy'\n\nconst DEFAULT_DECK_TITLE = 'Design Engineering & Web Accessibility (WCAG 2.2)'\n\nconst DEFAULT_CARDS: Flashcard[] = [\n  {\n    id: 'wcag-1-4-3',\n    category: 'WCAG Criterion 1.4.3',\n    difficulty: 'Level AA',\n    question: 'What is the minimum contrast ratio required for regular body text under WCAG AA standards?',\n    hint: 'Consider standard text (< 18pt or < 14pt bold) versus large display headings (≥ 18pt or ≥ 14pt bold).',\n    answer: '4.5:1 for normal text (< 18pt or < 14pt bold) and 3:1 for large text (≥ 18pt or ≥ 14pt bold).',\n    codeSnippet: 'color: oklch(0.20 0 0); /* on oklch(0.98 0 0) -> 14.2:1 passes AA/AAA */',\n    codeLanguage: 'css',\n    source: 'WCAG 2.2 § 1.4.3 Contrast (Minimum)',\n  },\n  {\n    id: 'wcag-2-5-8',\n    category: 'WCAG Criterion 2.5.8',\n    difficulty: 'Level AA',\n    question: 'What is the minimum interactive touch target size required in WCAG 2.2 Level AA?',\n    hint: 'Introduced in WCAG 2.2 to prevent accidental taps on mobile touchscreens without requiring 44×44px AAA.',\n    answer:\n      'At least 24×24 CSS pixels, or have sufficient spacing (undersized targets surrounded by at least 24px diameter clear space).',\n    codeSnippet: '<button class=\"min-h-6 min-w-6 p-2 touch-manipulation\">',\n    codeLanguage: 'html',\n    source: 'WCAG 2.2 § 2.5.8 Target Size (Minimum)',\n  },\n  {\n    id: 'wcag-2-4-7',\n    category: 'WCAG Criterion 2.4.7',\n    difficulty: 'Level AA',\n    question: 'What visual contrast requirement applies to keyboard focus indicators under WCAG 2.2?',\n    hint: 'Focus indicators must be distinguishable against both the focused element and adjacent background.',\n    answer:\n      'The focus indicator must have a contrast ratio of at least 3:1 against the unfocused state and surrounding background, with minimum 2px perimeter thickness.',\n    codeSnippet: 'focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',\n    codeLanguage: 'css',\n    source: 'WCAG 2.2 § 2.4.7 & 2.4.11 Focus Appearance',\n  },\n  {\n    id: 'wcag-4-1-2',\n    category: 'WCAG Criterion 4.1.2',\n    difficulty: 'Level A',\n    question: 'How should custom collapsible or expandable widgets communicate state to screen readers?',\n    hint: 'Use native semantic elements or explicit ARIA state attributes rather than visual classes alone.',\n    answer:\n      'Declare explicit ARIA roles (e.g. role=\"button\"), aria-expanded=\"true|false\", and aria-controls linking to the target element ID.',\n    codeSnippet: '<button aria-expanded=\"true\" aria-controls=\"card-back-content\">',\n    codeLanguage: 'html',\n    source: 'WAI-ARIA 1.2 Authoring Practices',\n  },\n  {\n    id: 'wcag-1-4-11',\n    category: 'WCAG Criterion 1.4.11',\n    difficulty: 'Level AA',\n    question: 'What is the contrast threshold for UI components, boundaries, and graphical objects?',\n    hint: 'Applies to active button borders, input outlines, checkboxes, and state badges.',\n    answer:\n      'A contrast ratio of at least 3:1 against adjacent colors for user interface components and graphical objects essential for comprehension.',\n    codeSnippet: 'border: 1px solid var(--border); /* >= 3:1 against surface */',\n    codeLanguage: 'css',\n    source: 'WCAG 2.2 § 1.4.11 Non-text Contrast',\n  },\n  {\n    id: 'oklch-tokens',\n    category: 'Design Systems & Color Science',\n    difficulty: 'Design System',\n    question: 'Why is the OKLCH color space preferred over HSL for dark mode and accessibility contrast derivation?',\n    hint: 'Perceptual lightness is uniform across all hues in OKLCH, unlike HSL where yellow looks brighter than blue at L=50%.',\n    answer:\n      'OKLCH is perceptually uniform. Equal changes in lightness (L) yield identical perceived contrast regardless of hue or chroma, preventing washed-out dark mode shifts.',\n    codeSnippet: '--color-primary: oklch(0.62 0.22 264.4);',\n    codeLanguage: 'css',\n    source: 'CSS Color Module Level 4',\n  },\n]\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-flashcard-study-deck, [ui-flashcard-study-deck]',\n  standalone: true,\n  // React renders the root <div> itself: the host stays out of layout and `class` goes to the root.\n  host: { '[attr.class]': '\"contents\"' },\n  imports: [\n    LucideAngularModule,\n    UiBadgeComponent,\n    UiButtonComponent,\n    UiCardComponent,\n    UiProgressComponent,\n    UiSeparatorComponent,\n  ],\n  template: `\n    <div data-slot=\"flashcard-study-deck\" [class]=\"rootClass\">\n      <!-- Header Section -->\n      <header class=\"space-y-4\">\n        <div class=\"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n          <div class=\"space-y-1\">\n            <div class=\"flex items-center gap-2\">\n              <span ui-badge variant=\"secondary\" class=\"gap-1 px-2.5 py-0.5 font-medium\">\n                <lucide-icon [img]=\"GraduationCap\" class=\"text-primary size-3.5\" />\n                <span>Study Deck</span>\n              </span>\n              <span ui-badge variant=\"outline\" class=\"text-muted-foreground gap-1 text-xs\">\n                <lucide-icon [img]=\"Flame\" class=\"text-warning size-3\" />\n                <span>5-day streak</span>\n              </span>\n            </div>\n            <h2 class=\"text-foreground text-xl font-bold tracking-tight md:text-2xl\">{{ deckTitle }}</h2>\n          </div>\n\n          <div class=\"flex items-center gap-2\">\n            <span ui-badge variant=\"outline\" class=\"px-3 py-1 font-mono text-xs font-medium\">\n              Card {{ currentNumber() }} of {{ totalCards() }} · {{ masteryPercentage() }}% Mastery\n            </span>\n          </div>\n        </div>\n\n        <!-- Progress Track -->\n        <div class=\"space-y-1.5\">\n          <div ui-progress [value]=\"progressPercentage()\" class=\"h-2\"></div>\n          <div class=\"text-muted-foreground flex items-center justify-between text-xs\">\n            <span>Progress: {{ currentNumber() }} / {{ totalCards() }} cards</span>\n            <span>{{ reviewedCount() }} of {{ totalCards() }} reviewed</span>\n          </div>\n        </div>\n      </header>\n\n      <!-- 3D Interactive Flashcard Container -->\n      @if (currentCard(); as card) {\n        <div class=\"mx-auto w-full max-w-2xl [perspective:1200px]\">\n          <div [class]=\"flipClass\">\n            <!-- FRONT FACE: Question State -->\n            <div ui-card [class]=\"frontCardClass\">\n              <!-- Front Header -->\n              <div class=\"flex items-center justify-between gap-3\">\n                <div class=\"flex flex-wrap items-center gap-2\">\n                  <span ui-badge variant=\"secondary\" class=\"text-xs font-medium\">{{ card.category }}</span>\n                  @if (card.difficulty) {\n                    <span ui-badge variant=\"outline\" class=\"text-xs\">{{ card.difficulty }}</span>\n                  }\n                </div>\n\n                <div class=\"flex items-center gap-1.5\">\n                  <button\n                    ui-button\n                    variant=\"ghost\"\n                    size=\"icon-sm\"\n                    [class]=\"bookmarkBtnClass\"\n                    aria-label=\"Bookmark card\"\n                    (click)=\"toggleBookmark($event)\"\n                  >\n                    <lucide-icon [img]=\"Bookmark\" [class]=\"bookmarkIconClass\" />\n                  </button>\n                </div>\n              </div>\n\n              <!-- Question Content -->\n              <div class=\"my-auto space-y-4 py-4\">\n                <div class=\"space-y-1.5\">\n                  <span class=\"text-muted-foreground text-xs font-semibold tracking-wider uppercase\">\n                    Question Prompt\n                  </span>\n                  <p class=\"text-foreground text-lg leading-snug font-semibold tracking-tight md:text-xl\">\n                    {{ card.question }}\n                  </p>\n                </div>\n\n                <!-- Hint Drawer / Toggle -->\n                @if (card.hint) {\n                  <div class=\"pt-2\">\n                    <button\n                      ui-button\n                      variant=\"ghost\"\n                      size=\"xs\"\n                      class=\"text-muted-foreground hover:text-foreground gap-1.5 text-xs\"\n                      (click)=\"toggleHint($event)\"\n                    >\n                      <lucide-icon [img]=\"Lightbulb\" class=\"text-warning size-3.5\" />\n                      <span>{{ showHint() ? 'Hide Hint' : 'Show Hint' }}</span>\n                      <span class=\"text-muted-foreground/60 font-mono\">[H]</span>\n                    </button>\n\n                    @if (showHint()) {\n                      <div\n                        class=\"text-muted-foreground border-warning/20 bg-warning/5 mt-2.5 flex items-start gap-2 rounded-lg border p-3 text-xs leading-relaxed\"\n                      >\n                        <lucide-icon [img]=\"Lightbulb\" class=\"text-warning mt-0.5 size-4 shrink-0\" />\n                        <div>\n                          <strong class=\"text-foreground font-semibold\">Hint: </strong>\n                          <span>{{ card.hint }}</span>\n                        </div>\n                      </div>\n                    }\n                  </div>\n                }\n              </div>\n\n              <!-- Front Footer / Flip Trigger -->\n              <div class=\"space-y-3 pt-2\">\n                <div ui-separator></div>\n                <div class=\"text-muted-foreground flex flex-col items-center justify-between gap-3 text-xs sm:flex-row\">\n                  <span class=\"hidden sm:inline\"\n                    >Press <kbd class=\"bg-muted border-border rounded border px-1 py-0.5 font-mono\">Space</kbd> or click\n                    button</span\n                  >\n                  <button\n                    ui-button\n                    variant=\"default\"\n                    size=\"sm\"\n                    class=\"w-full cursor-pointer gap-2 font-medium sm:w-auto\"\n                    (click)=\"toggleFlip($event)\"\n                  >\n                    <lucide-icon [img]=\"RotateCw\" class=\"size-3.5\" />\n                    <span>Flip Card to Reveal Answer</span>\n                  </button>\n                </div>\n              </div>\n            </div>\n\n            <!-- BACK FACE: Answer State -->\n            <div ui-card [class]=\"backCardClass\">\n              <!-- Back Header -->\n              <div class=\"flex items-center justify-between gap-3\">\n                <div class=\"flex flex-wrap items-center gap-2\">\n                  <span ui-badge variant=\"secondary\" class=\"text-xs font-medium\">{{ card.category }}</span>\n                  <span ui-badge variant=\"default\" class=\"border-success/30 bg-success/15 text-success text-xs\">\n                    Answer Revealed\n                  </span>\n                </div>\n\n                <button\n                  ui-button\n                  variant=\"ghost\"\n                  size=\"xs\"\n                  class=\"text-muted-foreground hover:text-foreground gap-1 text-xs\"\n                  (click)=\"toggleFlip($event)\"\n                >\n                  <lucide-icon [img]=\"RotateCw\" class=\"size-3.5\" />\n                  <span>Flip Back</span>\n                </button>\n              </div>\n\n              <!-- Answer Content -->\n              <div class=\"my-auto space-y-4 overflow-y-auto py-3\">\n                <div class=\"space-y-1.5\">\n                  <span class=\"text-primary text-xs font-semibold tracking-wider uppercase\"> Answer Explanation </span>\n                  <p class=\"text-foreground text-base leading-relaxed font-medium md:text-lg\">{{ card.answer }}</p>\n                </div>\n\n                <!-- Code / Token Example Block -->\n                @if (card.codeSnippet) {\n                  <div class=\"border-border bg-muted/60 dark:bg-muted/30 space-y-2 rounded-lg border p-3.5\">\n                    <div class=\"text-muted-foreground flex items-center justify-between text-xs\">\n                      <span class=\"font-mono font-medium\">Code / Token Example</span>\n                      <button\n                        ui-button\n                        variant=\"ghost\"\n                        size=\"xs\"\n                        class=\"h-6 gap-1 px-2 text-xs\"\n                        (click)=\"copyCodeSnippet($event)\"\n                      >\n                        @if (copiedSnippet()) {\n                          <lucide-icon [img]=\"Check\" class=\"text-success size-3\" />\n                        } @else {\n                          <lucide-icon [img]=\"Copy\" class=\"size-3\" />\n                        }\n                        <span>{{ copiedSnippet() ? 'Copied' : 'Copy' }}</span>\n                      </button>\n                    </div>\n                    <pre\n                      class=\"text-foreground bg-background/80 border-border/60 overflow-x-auto rounded border p-2 font-mono text-xs md:text-sm\"\n                    ><code>{{ card.codeSnippet }}</code></pre>\n                    @if (card.source) {\n                      <p class=\"text-muted-foreground text-xs italic\">Source: {{ card.source }}</p>\n                    }\n                  </div>\n                }\n              </div>\n\n              <!-- Spaced-Repetition Feedback Buttons -->\n              <div class=\"space-y-2.5 pt-2\">\n                <div class=\"text-muted-foreground flex items-center justify-between text-xs\">\n                  <span class=\"font-medium\">Rate recall difficulty (SM-2 Spaced Repetition):</span>\n                  @if (lastRatingFeedback()) {\n                    <span class=\"text-primary text-xs font-medium\">{{ lastRatingFeedback() }}</span>\n                  }\n                </div>\n\n                <div class=\"grid grid-cols-2 gap-2 sm:grid-cols-4\">\n                  <button\n                    ui-button\n                    variant=\"outline\"\n                    size=\"sm\"\n                    class=\"border-destructive/30 bg-destructive/10 text-destructive hover:bg-destructive/20 text-destructive flex h-auto flex-col py-2 text-xs active:scale-95\"\n                    (click)=\"handleRate('again', $event)\"\n                  >\n                    <span class=\"font-semibold\">Again</span>\n                    <span class=\"text-xs opacity-80\">&lt; 1 min [1]</span>\n                  </button>\n\n                  <button\n                    ui-button\n                    variant=\"outline\"\n                    size=\"sm\"\n                    class=\"border-warning/30 bg-warning/10 text-warning hover:bg-warning/20 text-warning flex h-auto flex-col py-2 text-xs active:scale-95\"\n                    (click)=\"handleRate('hard', $event)\"\n                  >\n                    <span class=\"font-semibold\">Hard</span>\n                    <span class=\"text-xs opacity-80\">12 hours [2]</span>\n                  </button>\n\n                  <button\n                    ui-button\n                    variant=\"outline\"\n                    size=\"sm\"\n                    class=\"border-info/30 bg-info/10 text-info hover:bg-info/20 text-info flex h-auto flex-col py-2 text-xs active:scale-95\"\n                    (click)=\"handleRate('good', $event)\"\n                  >\n                    <span class=\"font-semibold\">Good</span>\n                    <span class=\"text-xs opacity-80\">2 days [3]</span>\n                  </button>\n\n                  <button\n                    ui-button\n                    variant=\"outline\"\n                    size=\"sm\"\n                    class=\"border-success/30 bg-success/10 text-success hover:bg-success/20 text-success flex h-auto flex-col py-2 text-xs active:scale-95\"\n                    (click)=\"handleRate('easy', $event)\"\n                  >\n                    <span class=\"font-semibold\">Easy</span>\n                    <span class=\"text-xs opacity-80\">4 days [4]</span>\n                  </button>\n                </div>\n              </div>\n            </div>\n          </div>\n        </div>\n\n        <!-- Deck Navigation & Utility Controls -->\n        <div class=\"mx-auto flex max-w-2xl flex-col items-center justify-between gap-3 pt-2 sm:flex-row\">\n          <div class=\"flex w-full items-center justify-between gap-2 sm:w-auto sm:justify-start\">\n            <button\n              ui-button\n              variant=\"outline\"\n              size=\"sm\"\n              [disabled]=\"currentIndex() === 0\"\n              class=\"gap-1.5\"\n              (click)=\"handlePrev()\"\n            >\n              <lucide-icon [img]=\"ChevronLeft\" class=\"size-4\" />\n              <span>Previous</span>\n            </button>\n\n            <button ui-button variant=\"outline\" size=\"sm\" class=\"gap-1.5\" (click)=\"handleShuffle()\">\n              <lucide-icon [img]=\"Shuffle\" class=\"size-3.5\" />\n              <span>Shuffle</span>\n            </button>\n          </div>\n\n          <div class=\"flex w-full items-center justify-between gap-2 sm:w-auto sm:justify-end\">\n            <button ui-button variant=\"secondary\" size=\"sm\" class=\"gap-1.5\" (click)=\"toggleFlip($event)\">\n              <lucide-icon [img]=\"RotateCw\" class=\"size-3.5\" />\n              <span>{{ isFlipped() ? 'Show Question' : 'Flip Card' }}</span>\n            </button>\n\n            <button\n              ui-button\n              variant=\"default\"\n              size=\"sm\"\n              [disabled]=\"currentIndex() === totalCards() - 1\"\n              class=\"gap-1.5\"\n              (click)=\"handleNext()\"\n            >\n              <span>Next</span>\n              <lucide-icon [img]=\"ChevronRight\" class=\"size-4\" />\n            </button>\n          </div>\n        </div>\n\n        <!-- Keyboard Shortcuts Legend Footer -->\n        <div class=\"mx-auto max-w-2xl pt-2 text-center\">\n          <p class=\"text-muted-foreground font-mono text-xs\">\n            Shortcuts: <span class=\"text-foreground font-semibold\">← / →</span> Navigate ·\n            <span class=\"text-foreground font-semibold\">Space</span> Flip ·\n            <span class=\"text-foreground font-semibold\">H</span> Hint ·\n            <span class=\"text-foreground font-semibold\">1-4</span> Rate Interval\n          </p>\n        </div>\n      }\n    </div>\n  `,\n})\nexport class UiFlashcardStudyDeckComponent implements OnInit, OnDestroy {\n  protected readonly Bookmark = Bookmark\n  protected readonly Check = Check\n  protected readonly ChevronLeft = ChevronLeft\n  protected readonly ChevronRight = ChevronRight\n  protected readonly Copy = Copy\n  protected readonly Flame = Flame\n  protected readonly GraduationCap = GraduationCap\n  protected readonly Lightbulb = Lightbulb\n  protected readonly RotateCw = RotateCw\n  protected readonly Shuffle = Shuffle\n\n  @Input() initialTitle?: string\n  @Input() initialCards?: Flashcard[]\n  @Input('class') className?: string\n\n  readonly deck = signal<Flashcard[]>(DEFAULT_CARDS.map((c) => ({ ...c })))\n  readonly currentIndex = signal(0)\n  readonly isFlipped = signal(false)\n  readonly showHint = signal(false)\n  readonly copiedSnippet = signal(false)\n  readonly bookmarkedCards = signal<Set<string>>(new Set())\n  readonly cardScores = signal<Record<string, number>>({})\n  readonly lastRatingFeedback = signal<string | null>(null)\n\n  private timers: ReturnType<typeof setTimeout>[] = []\n  private onKeyDown = (event: KeyboardEvent): void => this.handleKeyDown(event)\n\n  constructor() {\n    inject(DestroyRef).onDestroy(() => {\n      for (const t of this.timers) clearTimeout(t)\n      if (typeof window !== 'undefined') window.removeEventListener('keydown', this.onKeyDown)\n    })\n  }\n\n  ngOnInit(): void {\n    if (this.initialCards) this.deck.set(this.initialCards.map((c) => ({ ...c })))\n    if (typeof window !== 'undefined') window.addEventListener('keydown', this.onKeyDown)\n  }\n\n  ngOnDestroy(): void {\n    if (typeof window !== 'undefined') window.removeEventListener('keydown', this.onKeyDown)\n  }\n\n  readonly currentCard = computed(() => this.deck()[this.currentIndex()] ?? this.deck()[0])\n  readonly totalCards = computed(() => this.deck().length)\n  readonly currentNumber = computed(() => this.currentIndex() + 1)\n  readonly progressPercentage = computed(() => Math.round((this.currentNumber() / this.totalCards()) * 100))\n  readonly reviewedCount = computed(() => Object.keys(this.cardScores()).length)\n\n  readonly masteryPercentage = computed(() => {\n    const keys = Object.keys(this.cardScores())\n    if (keys.length === 0) return 0\n    const total = keys.reduce((sum, key) => sum + (this.cardScores()[key] ?? 0), 0)\n    return Math.min(100, Math.round(total / this.totalCards()))\n  })\n\n  readonly isBookmarked = computed(() => {\n    const card = this.currentCard()\n    return card ? this.bookmarkedCards().has(card.id) : false\n  })\n\n  get deckTitle(): string {\n    return this.initialTitle ?? DEFAULT_DECK_TITLE\n  }\n\n  get rootClass(): string {\n    return cn('mx-auto w-full max-w-4xl space-y-6 p-4 md:p-6 lg:p-8', this.className)\n  }\n\n  get flipClass(): string {\n    return cn(\n      'relative min-h-[440px] w-full rounded-xl transition-transform duration-500 [transform-style:preserve-3d] md:min-h-[460px]',\n      this.isFlipped() && '[transform:rotateY(180deg)]',\n    )\n  }\n\n  get frontCardClass(): string {\n    return cn(\n      'border-border bg-card absolute inset-0 flex h-full w-full flex-col justify-between rounded-xl border p-6 shadow-xs [backface-visibility:hidden] md:p-8',\n      this.isFlipped() ? 'pointer-events-none opacity-0' : 'pointer-events-auto opacity-100',\n    )\n  }\n\n  get backCardClass(): string {\n    return cn(\n      'border-border bg-card absolute inset-0 flex h-full w-full [transform:rotateY(180deg)] flex-col justify-between rounded-xl border p-6 shadow-xs [backface-visibility:hidden] md:p-8',\n      this.isFlipped() ? 'pointer-events-auto opacity-100' : 'pointer-events-none opacity-0',\n    )\n  }\n\n  get bookmarkBtnClass(): string {\n    return cn('text-muted-foreground hover:text-foreground', this.isBookmarked() && 'text-warning')\n  }\n\n  get bookmarkIconClass(): string {\n    return cn('size-4', this.isBookmarked() && 'fill-warning text-warning')\n  }\n\n  toggleFlip(event?: Event): void {\n    event?.stopPropagation()\n    this.isFlipped.update((v) => !v)\n  }\n\n  toggleHint(event?: Event): void {\n    event?.stopPropagation()\n    this.showHint.update((v) => !v)\n  }\n\n  toggleBookmark(event?: Event): void {\n    event?.stopPropagation()\n    const card = this.currentCard()\n    if (!card) return\n    this.bookmarkedCards.update((prev) => {\n      const next = new Set(prev)\n      if (next.has(card.id)) next.delete(card.id)\n      else next.add(card.id)\n      return next\n    })\n  }\n\n  handlePrev(): void {\n    if (this.currentIndex() > 0) {\n      this.currentIndex.update((i) => i - 1)\n      this.isFlipped.set(false)\n      this.showHint.set(false)\n      this.lastRatingFeedback.set(null)\n    }\n  }\n\n  handleNext(): void {\n    if (this.currentIndex() < this.totalCards() - 1) {\n      this.currentIndex.update((i) => i + 1)\n      this.isFlipped.set(false)\n      this.showHint.set(false)\n      this.lastRatingFeedback.set(null)\n    }\n  }\n\n  handleShuffle(): void {\n    const shuffled = [...this.deck()]\n    for (let i = shuffled.length - 1; i > 0; i--) {\n      const j = Math.floor(Math.random() * (i + 1))\n      ;[shuffled[i], shuffled[j]] = [shuffled[j]!, shuffled[i]!]\n    }\n    this.deck.set(shuffled)\n    this.currentIndex.set(0)\n    this.isFlipped.set(false)\n    this.showHint.set(false)\n    this.lastRatingFeedback.set('Deck shuffled')\n    this.timers.push(\n      setTimeout(() => {\n        if (this.lastRatingFeedback() === 'Deck shuffled') this.lastRatingFeedback.set(null)\n      }, 2000),\n    )\n  }\n\n  handleRate(interval: RatingInterval, event?: Event): void {\n    event?.stopPropagation()\n    const card = this.currentCard()\n    if (!card) return\n    const scoreMap: Record<RatingInterval, { score: number; label: string }> = {\n      again: { score: 25, label: 'Again (< 1 min)' },\n      hard: { score: 50, label: 'Hard (12 hours)' },\n      good: { score: 80, label: 'Good (2 days)' },\n      easy: { score: 100, label: 'Easy (4 days)' },\n    }\n    const { score, label } = scoreMap[interval]\n    this.cardScores.update((prev) => ({ ...prev, [card.id]: score }))\n    this.lastRatingFeedback.set(`Rated: ${label}`)\n    this.timers.push(\n      setTimeout(() => {\n        if (this.currentIndex() < this.totalCards() - 1) this.handleNext()\n        else {\n          this.isFlipped.set(false)\n          this.showHint.set(false)\n        }\n      }, 350),\n    )\n  }\n\n  async copyCodeSnippet(event?: Event): Promise<void> {\n    event?.stopPropagation()\n    const snippet = this.currentCard()?.codeSnippet\n    if (!snippet) return\n    try {\n      await navigator.clipboard.writeText(snippet)\n      this.copiedSnippet.set(true)\n      this.timers.push(setTimeout(() => this.copiedSnippet.set(false), 2000))\n    } catch {\n      // Clipboard permission fallback\n    }\n  }\n\n  private handleKeyDown(event: KeyboardEvent): void {\n    const target = event.target as HTMLElement | null\n    if (target && ['INPUT', 'TEXTAREA', 'SELECT'].includes(target.tagName)) return\n    if (event.key === 'ArrowLeft') {\n      event.preventDefault()\n      this.handlePrev()\n    } else if (event.key === 'ArrowRight') {\n      event.preventDefault()\n      this.handleNext()\n    } else if (event.key === ' ' || event.code === 'Space') {\n      event.preventDefault()\n      this.toggleFlip()\n    } else if (event.key.toLowerCase() === 'h') {\n      event.preventDefault()\n      this.toggleHint()\n    } else if (this.isFlipped()) {\n      if (event.key === '1') {\n        event.preventDefault()\n        this.handleRate('again')\n      } else if (event.key === '2') {\n        event.preventDefault()\n        this.handleRate('hard')\n      } else if (event.key === '3') {\n        event.preventDefault()\n        this.handleRate('good')\n      } else if (event.key === '4') {\n        event.preventDefault()\n        this.handleRate('easy')\n      }\n    }\n  }\n}\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/flashcard-study-deck/flashcard-study-deck.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/flashcard-study-deck/index.ts",
      "content": "export { UiFlashcardStudyDeckComponent, type Flashcard, type RatingInterval } from './flashcard-study-deck.component'\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/flashcard-study-deck/index.ts"
    }
  ],
  "dependencies": [
    "lucide-angular"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/badge.json",
    "https://uipkge.dev/r/angular/button.json",
    "https://uipkge.dev/r/angular/card.json",
    "https://uipkge.dev/r/angular/progress.json",
    "https://uipkge.dev/r/angular/separator.json"
  ],
  "description": "Anki and Quizlet style spaced-repetition flashcard study deck with smooth 3D flip card interaction, WCAG 2.2 accessibility curriculum, hint drawer, mastery percentage tracking, keyboard shortcuts, and SM-2 interval rating buttons.",
  "categories": [
    "education",
    "app"
  ]
}