{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "flashcard-study-deck",
  "title": "Flashcard Study Deck",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-vue/blocks/flashcard-study-deck/FlashcardStudyDeck.vue",
      "content": "<script setup lang=\"ts\">\nimport type { HTMLAttributes } from 'vue'\nimport { computed, onMounted, onUnmounted, ref } from 'vue'\nimport {\n  Bookmark,\n  Check,\n  ChevronLeft,\n  ChevronRight,\n  Copy,\n  Flame,\n  GraduationCap,\n  Lightbulb,\n  RotateCw,\n  Shuffle,\n} from 'lucide-vue-next'\nimport { cn } from '@/lib/utils'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport { Card } from '@/components/ui/card'\nimport { Progress } from '@/components/ui/progress'\nimport { Separator } from '@/components/ui/separator'\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\ninterface Props {\n  initialTitle?: string\n  initialCards?: Flashcard[]\n  class?: HTMLAttributes['class']\n}\n\nconst props = defineProps<Props>()\n\nconst deckTitle = computed(() => props.initialTitle ?? DEFAULT_DECK_TITLE)\nconst deck = ref<Flashcard[]>([...(props.initialCards ?? DEFAULT_CARDS)])\nconst currentIndex = ref(0)\nconst isFlipped = ref(false)\nconst showHint = ref(false)\nconst copiedSnippet = ref(false)\nconst bookmarkedCards = ref<Set<string>>(new Set())\nconst cardScores = ref<Record<string, number>>({})\nconst lastRatingFeedback = ref<string | null>(null)\n\nconst currentCard = computed(() => deck.value[currentIndex.value] ?? deck.value[0])\n\nconst totalCards = computed(() => deck.value.length)\nconst currentNumber = computed(() => currentIndex.value + 1)\nconst progressPercentage = computed(() => Math.round((currentNumber.value / totalCards.value) * 100))\n\nconst masteryPercentage = computed(() => {\n  const ratedKeys = Object.keys(cardScores.value)\n  if (ratedKeys.length === 0) return 0\n  const totalScore = ratedKeys.reduce((sum, key) => sum + (cardScores.value[key] ?? 0), 0)\n  return Math.min(100, Math.round(totalScore / totalCards.value))\n})\n\nconst isBookmarked = computed(() => bookmarkedCards.value.has(currentCard.value?.id))\n\nfunction toggleFlip() {\n  isFlipped.value = !isFlipped.value\n}\n\nfunction toggleHint() {\n  showHint.value = !showHint.value\n}\n\nfunction toggleBookmark() {\n  if (!currentCard.value) return\n  const id = currentCard.value.id\n  if (bookmarkedCards.value.has(id)) {\n    bookmarkedCards.value.delete(id)\n  } else {\n    bookmarkedCards.value.add(id)\n  }\n}\n\nfunction handlePrev() {\n  if (currentIndex.value > 0) {\n    currentIndex.value--\n    isFlipped.value = false\n    showHint.value = false\n    lastRatingFeedback.value = null\n  }\n}\n\nfunction handleNext() {\n  if (currentIndex.value < totalCards.value - 1) {\n    currentIndex.value++\n    isFlipped.value = false\n    showHint.value = false\n    lastRatingFeedback.value = null\n  }\n}\n\nfunction handleShuffle() {\n  const shuffled = [...deck.value]\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  deck.value = shuffled\n  currentIndex.value = 0\n  isFlipped.value = false\n  showHint.value = false\n  lastRatingFeedback.value = 'Deck shuffled'\n  setTimeout(() => {\n    if (lastRatingFeedback.value === 'Deck shuffled') {\n      lastRatingFeedback.value = null\n    }\n  }, 2000)\n}\n\nfunction handleRate(interval: RatingInterval) {\n  if (!currentCard.value) return\n\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\n  const { score, label } = scoreMap[interval]\n  cardScores.value[currentCard.value.id] = score\n  lastRatingFeedback.value = `Rated: ${label}`\n\n  setTimeout(() => {\n    if (currentIndex.value < totalCards.value - 1) {\n      handleNext()\n    } else {\n      isFlipped.value = false\n      showHint.value = false\n    }\n  }, 350)\n}\n\nasync function copyCodeSnippet() {\n  if (!currentCard.value?.codeSnippet) return\n  try {\n    await navigator.clipboard.writeText(currentCard.value.codeSnippet)\n    copiedSnippet.value = true\n    setTimeout(() => {\n      copiedSnippet.value = false\n    }, 2000)\n  } catch {\n    // Clipboard permission fallback\n  }\n}\n\nfunction handleKeyDown(event: KeyboardEvent) {\n  const target = event.target as HTMLElement | null\n  if (target && ['INPUT', 'TEXTAREA', 'SELECT'].includes(target.tagName)) return\n\n  if (event.key === 'ArrowLeft') {\n    event.preventDefault()\n    handlePrev()\n  } else if (event.key === 'ArrowRight') {\n    event.preventDefault()\n    handleNext()\n  } else if (event.key === ' ' || event.code === 'Space') {\n    event.preventDefault()\n    toggleFlip()\n  } else if (event.key.toLowerCase() === 'h') {\n    event.preventDefault()\n    toggleHint()\n  } else if (isFlipped.value) {\n    if (event.key === '1') {\n      event.preventDefault()\n      handleRate('again')\n    } else if (event.key === '2') {\n      event.preventDefault()\n      handleRate('hard')\n    } else if (event.key === '3') {\n      event.preventDefault()\n      handleRate('good')\n    } else if (event.key === '4') {\n      event.preventDefault()\n      handleRate('easy')\n    }\n  }\n}\n\nonMounted(() => {\n  window.addEventListener('keydown', handleKeyDown)\n})\n\nonUnmounted(() => {\n  window.removeEventListener('keydown', handleKeyDown)\n})\n</script>\n\n<template>\n  <div\n    data-slot=\"flashcard-study-deck\"\n    :class=\"cn('mx-auto w-full max-w-4xl space-y-6 p-4 md:p-6 lg:p-8', props.class)\"\n  >\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            <Badge variant=\"secondary\" class=\"gap-1 px-2.5 py-0.5 font-medium\">\n              <GraduationCap class=\"text-primary size-3.5\" />\n              <span>Study Deck</span>\n            </Badge>\n            <Badge variant=\"outline\" class=\"text-muted-foreground gap-1 text-xs\">\n              <Flame class=\"text-warning size-3\" />\n              <span>5-day streak</span>\n            </Badge>\n          </div>\n          <h2 class=\"text-foreground text-xl font-bold tracking-tight md:text-2xl\">\n            {{ deckTitle }}\n          </h2>\n        </div>\n\n        <div class=\"flex items-center gap-2\">\n          <Badge variant=\"outline\" class=\"px-3 py-1 font-mono text-xs font-medium\">\n            Card {{ currentNumber }} of {{ totalCards }} · {{ masteryPercentage }}% Mastery\n          </Badge>\n        </div>\n      </div>\n\n      <!-- Progress Track -->\n      <div class=\"space-y-1.5\">\n        <Progress :model-value=\"progressPercentage\" class=\"h-2\" />\n        <div class=\"text-muted-foreground flex items-center justify-between text-xs\">\n          <span>Progress: {{ currentNumber }} / {{ totalCards }} cards</span>\n          <span>{{ Object.keys(cardScores).length }} of {{ totalCards }} reviewed</span>\n        </div>\n      </div>\n    </header>\n\n    <!-- 3D Interactive Flashcard Container -->\n    <div class=\"mx-auto w-full max-w-2xl [perspective:1200px]\">\n      <div\n        :class=\"\n          cn(\n            'relative min-h-[440px] w-full rounded-xl transition-transform duration-500 [transform-style:preserve-3d] md:min-h-[460px]',\n            isFlipped && '[transform:rotateY(180deg)]',\n          )\n        \"\n      >\n        <!-- FRONT FACE: Question State -->\n        <Card\n          :class=\"\n            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              isFlipped ? 'pointer-events-none opacity-0' : 'pointer-events-auto opacity-100',\n            )\n          \"\n        >\n          <!-- Front Header -->\n          <div class=\"flex items-center justify-between gap-3\">\n            <div class=\"flex flex-wrap items-center gap-2\">\n              <Badge variant=\"secondary\" class=\"text-xs font-medium\">\n                {{ currentCard.category }}\n              </Badge>\n              <Badge v-if=\"currentCard.difficulty\" variant=\"outline\" class=\"text-xs\">\n                {{ currentCard.difficulty }}\n              </Badge>\n            </div>\n\n            <div class=\"flex items-center gap-1.5\">\n              <Button\n                variant=\"ghost\"\n                size=\"icon-sm\"\n                :class=\"cn('text-muted-foreground hover:text-foreground', isBookmarked && 'text-warning')\"\n                aria-label=\"Bookmark card\"\n                @click.stop=\"toggleBookmark\"\n              >\n                <Bookmark :class=\"cn('size-4', isBookmarked && 'fill-warning text-warning')\" />\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                {{ currentCard.question }}\n              </p>\n            </div>\n\n            <!-- Hint Drawer / Toggle -->\n            <div v-if=\"currentCard.hint\" class=\"pt-2\">\n              <Button\n                variant=\"ghost\"\n                size=\"xs\"\n                class=\"text-muted-foreground hover:text-foreground gap-1.5 text-xs\"\n                @click.stop=\"toggleHint\"\n              >\n                <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              <div\n                v-if=\"showHint\"\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                <Lightbulb class=\"text-warning mt-0.5 size-4 shrink-0\" />\n                <div>\n                  <strong class=\"text-foreground font-semibold\">Hint: </strong>\n                  <span>{{ currentCard.hint }}</span>\n                </div>\n              </div>\n            </div>\n          </div>\n\n          <!-- Front Footer / Flip Trigger -->\n          <div class=\"space-y-3 pt-2\">\n            <Separator />\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                variant=\"default\"\n                size=\"sm\"\n                class=\"w-full cursor-pointer gap-2 font-medium sm:w-auto\"\n                @click.stop=\"toggleFlip\"\n              >\n                <RotateCw class=\"size-3.5\" />\n                <span>Flip Card to Reveal Answer</span>\n              </Button>\n            </div>\n          </div>\n        </Card>\n\n        <!-- BACK FACE: Answer State -->\n        <Card\n          :class=\"\n            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              isFlipped ? 'pointer-events-auto opacity-100' : 'pointer-events-none opacity-0',\n            )\n          \"\n        >\n          <!-- Back Header -->\n          <div class=\"flex items-center justify-between gap-3\">\n            <div class=\"flex flex-wrap items-center gap-2\">\n              <Badge variant=\"secondary\" class=\"text-xs font-medium\">\n                {{ currentCard.category }}\n              </Badge>\n              <Badge variant=\"default\" class=\"border-success/30 bg-success/15 text-success text-xs\">\n                Answer Revealed\n              </Badge>\n            </div>\n\n            <Button\n              variant=\"ghost\"\n              size=\"xs\"\n              class=\"text-muted-foreground hover:text-foreground gap-1 text-xs\"\n              @click.stop=\"toggleFlip\"\n            >\n              <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\">\n                {{ currentCard.answer }}\n              </p>\n            </div>\n\n            <!-- Code / Token Example Block -->\n            <div\n              v-if=\"currentCard.codeSnippet\"\n              class=\"border-border bg-muted/60 dark:bg-muted/30 space-y-2 rounded-lg border p-3.5\"\n            >\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 variant=\"ghost\" size=\"xs\" class=\"h-6 gap-1 px-2 text-xs\" @click.stop=\"copyCodeSnippet\">\n                  <Check v-if=\"copiedSnippet\" class=\"text-success size-3\" />\n                  <Copy v-else class=\"size-3\" />\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>{{ currentCard.codeSnippet }}</code></pre>\n              <p v-if=\"currentCard.source\" class=\"text-muted-foreground text-xs italic\">\n                Source: {{ currentCard.source }}\n              </p>\n            </div>\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              <span v-if=\"lastRatingFeedback\" class=\"text-primary text-xs font-medium\">\n                {{ lastRatingFeedback }}\n              </span>\n            </div>\n\n            <div class=\"grid grid-cols-2 gap-2 sm:grid-cols-4\">\n              <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.stop=\"handleRate('again')\"\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                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.stop=\"handleRate('hard')\"\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                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.stop=\"handleRate('good')\"\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                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.stop=\"handleRate('easy')\"\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        </Card>\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 variant=\"outline\" size=\"sm\" :disabled=\"currentIndex === 0\" class=\"gap-1.5\" @click=\"handlePrev\">\n          <ChevronLeft class=\"size-4\" />\n          <span>Previous</span>\n        </Button>\n\n        <Button variant=\"outline\" size=\"sm\" class=\"gap-1.5\" @click=\"handleShuffle\">\n          <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 variant=\"secondary\" size=\"sm\" class=\"gap-1.5\" @click=\"toggleFlip\">\n          <RotateCw class=\"size-3.5\" />\n          <span>{{ isFlipped ? 'Show Question' : 'Flip Card' }}</span>\n        </Button>\n\n        <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          <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  </div>\n</template>\n",
      "type": "registry:block",
      "target": "~/app/components/blocks/FlashcardStudyDeck.vue"
    }
  ],
  "dependencies": [
    "lucide-vue-next"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/vue/badge.json",
    "https://uipkge.dev/r/vue/button.json",
    "https://uipkge.dev/r/vue/card.json",
    "https://uipkge.dev/r/vue/progress.json",
    "https://uipkge.dev/r/vue/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"
  ]
}