{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "habit-streak-tracker",
  "title": "Habit Streak Tracker",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-angular/blocks/habit-streak-tracker/habit-streak-tracker.component.ts",
      "content": "// GitHub style 52-week contribution habit heatmap, flame streak counter, consistency\n// metrics, and daily completion tracker with 7-day mini checkbox strip and progress bars.\n// Port of the React block 1:1.\nimport { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core'\nimport {\n  BookOpen,\n  Calendar,\n  Check,\n  CheckCircle2,\n  CodeXml,\n  Droplets,\n  Flame,\n  Footprints,\n  LucideAngularModule,\n  Plus,\n  Target,\n  Terminal,\n  TrendingUp,\n  Trophy,\n  X,\n  type LucideIconData,\n} from 'lucide-angular'\nimport { cn } from '@/lib/utils'\nimport { UiBadgeComponent } from '@/ui/badge/badge.component'\nimport { UiButtonComponent } from '@/ui/button/button.component'\nimport {\n  UiCardComponent,\n  UiCardContentComponent,\n  UiCardDescriptionComponent,\n  UiCardHeaderComponent,\n  UiCardTitleComponent,\n} from '@/ui/card/card.component'\nimport { UiProgressComponent } from '@/ui/progress/progress.component'\n\nexport type HabitCategory = 'Engineering' | 'Health' | 'Learning' | 'Mindset' | string\n\nexport interface HabitItem {\n  id: string\n  title: string\n  description: string\n  category: HabitCategory\n  icon: 'code' | 'terminal' | 'steps' | 'book' | 'water' | 'target' | string\n  streakDays: number\n  completionRate: number\n  weekHistory: boolean[]\n}\n\nexport interface DayHeatmapCell {\n  date: string\n  formattedDate: string\n  dayName: string\n  dayIndex: number\n  weekIndex: number\n  count: number\n  level: number\n  month: string\n}\n\nexport interface MonthLabel {\n  name: string\n  weekIndex: number\n}\n\nconst defaultHabits: HabitItem[] = [\n  {\n    id: 'habit-1',\n    title: 'Ship 1 Component to Registry',\n    description: 'Author, test, and register a new UI block or primitive',\n    category: 'Engineering',\n    icon: 'code',\n    streakDays: 24,\n    completionRate: 96,\n    weekHistory: [true, true, true, true, true, true, true],\n  },\n  {\n    id: 'habit-2',\n    title: '1 Hour Deep Work / Code Review',\n    description: 'Uninterrupted architecture focus & PR review rounds',\n    category: 'Engineering',\n    icon: 'terminal',\n    streakDays: 19,\n    completionRate: 92,\n    weekHistory: [true, true, true, true, true, false, true],\n  },\n  {\n    id: 'habit-3',\n    title: '10,000 Steps Walking',\n    description: 'Daily outdoor walk & cardiovascular activity',\n    category: 'Health',\n    icon: 'steps',\n    streakDays: 24,\n    completionRate: 100,\n    weekHistory: [true, true, true, true, true, true, true],\n  },\n  {\n    id: 'habit-4',\n    title: 'Read 20 Pages Tech Book',\n    description: 'Systems design, TypeScript craft, or CS fundamentals',\n    category: 'Learning',\n    icon: 'book',\n    streakDays: 12,\n    completionRate: 85,\n    weekHistory: [true, true, false, true, true, true, false],\n  },\n  {\n    id: 'habit-5',\n    title: 'Drink 2.5L Water',\n    description: 'Optimal hydration tracking throughout the working day',\n    category: 'Health',\n    icon: 'water',\n    streakDays: 30,\n    completionRate: 98,\n    weekHistory: [true, true, true, true, true, true, true],\n  },\n]\n\nexport function generate52WeekHeatmap(baseStreakDays = 24): {\n  weeks: DayHeatmapCell[][]\n  monthLabels: MonthLabel[]\n  totalCompletions: number\n} {\n  const weeks: DayHeatmapCell[][] = []\n  const monthLabels: MonthLabel[] = []\n  const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']\n  const dayNames = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']\n  const endDate = new Date(2026, 7, 23)\n  let lastMonth = ''\n  let totalCompletions = 0\n  let seed = 1337\n  function pseudoRandom(): number {\n    seed = (seed * 9301 + 49297) % 233280\n    return seed / 233280\n  }\n  for (let w = 0; w < 52; w++) {\n    const weekDays: DayHeatmapCell[] = []\n    for (let d = 0; d < 7; d++) {\n      const daysAgo = (51 - w) * 7 + (6 - d)\n      const dateObj = new Date(endDate)\n      dateObj.setDate(endDate.getDate() - daysAgo)\n      const monthName = months[dateObj.getMonth()] ?? 'Jan'\n      const formattedDate = `${monthName} ${dateObj.getDate()}, ${dateObj.getFullYear()}`\n      const dateStr = dateObj.toISOString().split('T')[0] ?? `w${w}d${d}`\n      let count = 0\n      if (daysAgo <= baseStreakDays && daysAgo >= 0) {\n        count = pseudoRandom() > 0.3 ? 5 : 4\n      } else if (daysAgo >= 90 && daysAgo <= 138) {\n        count = pseudoRandom() > 0.2 ? 5 : 4\n      } else {\n        const rand = pseudoRandom()\n        if (rand > 0.82) count = 5\n        else if (rand > 0.48) count = 4\n        else if (rand > 0.22) count = 3\n        else if (rand > 0.08) count = 2\n        else if (rand > 0.03) count = 1\n        else count = 0\n      }\n      totalCompletions += count\n      let level = 0\n      if (count >= 5) level = 4\n      else if (count === 4) level = 3\n      else if (count === 3) level = 2\n      else if (count >= 1) level = 1\n      else level = 0\n      weekDays.push({\n        date: dateStr,\n        formattedDate,\n        dayName: dayNames[d] ?? '',\n        dayIndex: d,\n        weekIndex: w,\n        count,\n        level,\n        month: monthName,\n      })\n      if (d === 0 && monthName !== lastMonth) {\n        monthLabels.push({ name: monthName, weekIndex: w })\n        lastMonth = monthName\n      }\n    }\n    weeks.push(weekDays)\n  }\n  return { weeks, monthLabels, totalCompletions }\n}\n\nconst weekDayColumns = [\n  { short: 'Mon', num: '17' },\n  { short: 'Tue', num: '18' },\n  { short: 'Wed', num: '19' },\n  { short: 'Thu', num: '20' },\n  { short: 'Fri', num: '21' },\n  { short: 'Sat', num: '22' },\n  { short: 'Sun', num: '23' },\n]\n\nconst levelClassMap: Record<number, string> = {\n  0: 'bg-muted/60 dark:bg-muted/30 border border-border/30 hover:ring-1 hover:ring-foreground/20',\n  1: 'bg-success/25 border border-success/20 hover:ring-1 hover:ring-success/50',\n  2: 'bg-success/50 border border-success/30 hover:ring-1 hover:ring-success/70',\n  3: 'bg-success/75 border border-success/40 hover:ring-1 hover:ring-success/90',\n  4: 'bg-success border border-success/60 hover:ring-1 hover:ring-success',\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-habit-streak-tracker, [ui-habit-streak-tracker]',\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    UiCardContentComponent,\n    UiCardDescriptionComponent,\n    UiCardHeaderComponent,\n    UiCardTitleComponent,\n    UiProgressComponent,\n  ],\n  template: `\n    <div data-slot=\"habit-streak-tracker\" [class]=\"rootClass\">\n      <!-- Header -->\n      <div\n        class=\"border-border/80 bg-card text-card-foreground flex flex-col gap-4 rounded-xl border p-4 shadow-xs sm:p-5 md:flex-row md:items-center md:justify-between\"\n      >\n        <div class=\"flex items-start gap-3.5 sm:items-center\">\n          <div\n            class=\"border-warning/30 bg-warning/10 text-warning text-warning flex size-10 shrink-0 items-center justify-center rounded-xl border shadow-xs\"\n          >\n            <lucide-icon [img]=\"Flame\" class=\"size-5.5 animate-pulse\" />\n          </div>\n          <div class=\"space-y-1\">\n            <div class=\"flex flex-wrap items-center gap-2.5\">\n              <h1 class=\"text-foreground text-base leading-none font-semibold tracking-tight sm:text-lg\">\n                Daily Habits & Consistency Matrix\n              </h1>\n              <span\n                ui-badge\n                variant=\"warning\"\n                class=\"border-warning/30 bg-warning/15 text-warning gap-1 text-xs font-semibold\"\n              >\n                <lucide-icon [img]=\"Flame\" class=\"fill-warning text-warning size-3.5\" />\n                <span class=\"font-mono tabular-nums\">🔥 {{ initialStreak() }}-Day Streak · Personal Best!</span>\n              </span>\n            </div>\n            <p class=\"text-muted-foreground text-xs\">\n              {{ monthTitle() }} · Real-time habit adherence, 52-week GitHub matrix, and routine tracker.\n            </p>\n          </div>\n        </div>\n        <div class=\"flex flex-wrap items-center gap-2.5 self-end sm:self-auto\">\n          <div\n            class=\"border-border/70 bg-muted/40 text-muted-foreground flex items-center gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs font-medium\"\n          >\n            <lucide-icon [img]=\"Calendar\" class=\"size-3.5\" />\n            <span>{{ monthTitle() }}</span>\n          </div>\n          <button\n            ui-button\n            aria-label=\"Close new habit dialog\"\n            size=\"sm\"\n            class=\"gap-1.5 shadow-xs\"\n            (click)=\"toggleModal()\"\n          >\n            <lucide-icon [img]=\"Plus\" class=\"size-4\" />\n            <span>New Habit</span>\n          </button>\n        </div>\n      </div>\n\n      <!-- Quick Add -->\n      @if (showNewHabitModal()) {\n        <div class=\"border-border/80 bg-card text-card-foreground space-y-3 rounded-xl border p-4 shadow-xs sm:p-5\">\n          <div class=\"flex items-center justify-between\">\n            <div class=\"space-y-0.5\">\n              <p class=\"text-foreground text-sm font-semibold\">Create New Habit Routine</p>\n              <p class=\"text-muted-foreground text-xs\">Define a daily target to track against your annual matrix.</p>\n            </div>\n            <button\n              ui-button\n              variant=\"ghost\"\n              size=\"icon-sm\"\n              class=\"size-7\"\n              aria-label=\"Close new habit dialog\"\n              (click)=\"showNewHabitModal.set(false)\"\n            >\n              <lucide-icon [img]=\"X\" class=\"size-4\" />\n            </button>\n          </div>\n          <div class=\"grid grid-cols-1 gap-3 sm:grid-cols-12\">\n            <div class=\"sm:col-span-4\">\n              <label class=\"text-foreground mb-1 block text-xs font-medium\">Habit Title</label>\n              <input\n                type=\"text\"\n                [value]=\"newHabitTitle()\"\n                (input)=\"newHabitTitle.set($any($event.target).value)\"\n                (keydown.enter)=\"handleAddHabit()\"\n                placeholder=\"e.g. Write 500 Words Technical Notes\"\n                class=\"border-border bg-background placeholder:text-muted-foreground focus-visible:ring-ring h-8 w-full rounded-md border px-2.5 text-xs focus-visible:ring-2 focus-visible:outline-none\"\n              />\n            </div>\n            <div class=\"sm:col-span-4\">\n              <label class=\"text-foreground mb-1 block text-xs font-medium\">Description</label>\n              <input\n                type=\"text\"\n                [value]=\"newHabitDescription()\"\n                (input)=\"newHabitDescription.set($any($event.target).value)\"\n                (keydown.enter)=\"handleAddHabit()\"\n                placeholder=\"e.g. Daily engineering documentation\"\n                class=\"border-border bg-background placeholder:text-muted-foreground focus-visible:ring-ring h-8 w-full rounded-md border px-2.5 text-xs focus-visible:ring-2 focus-visible:outline-none\"\n              />\n            </div>\n            <div class=\"sm:col-span-2\">\n              <label class=\"text-foreground mb-1 block text-xs font-medium\">Category</label>\n              <select\n                [value]=\"newHabitCategory()\"\n                (change)=\"newHabitCategory.set($any($event.target).value)\"\n                class=\"border-border bg-background text-foreground focus-visible:ring-ring h-8 w-full rounded-md border px-2 text-xs focus-visible:ring-2 focus-visible:outline-none\"\n              >\n                <option value=\"Engineering\">Engineering</option>\n                <option value=\"Health\">Health</option>\n                <option value=\"Learning\">Learning</option>\n                <option value=\"Mindset\">Mindset</option>\n              </select>\n            </div>\n            <div class=\"sm:col-span-2\">\n              <label class=\"text-foreground mb-1 block text-xs font-medium\">Icon</label>\n              <select\n                [value]=\"newHabitIcon()\"\n                (change)=\"newHabitIcon.set($any($event.target).value)\"\n                class=\"border-border bg-background text-foreground focus-visible:ring-ring h-8 w-full rounded-md border px-2 text-xs focus-visible:ring-2 focus-visible:outline-none\"\n              >\n                <option value=\"code\">Code</option>\n                <option value=\"terminal\">Terminal</option>\n                <option value=\"steps\">Steps</option>\n                <option value=\"book\">Book</option>\n                <option value=\"water\">Water</option>\n                <option value=\"target\">Target</option>\n              </select>\n            </div>\n          </div>\n          <div class=\"flex items-center justify-end gap-2 pt-1\">\n            <button\n              ui-button\n              aria-label=\"Close new habit dialog\"\n              variant=\"ghost\"\n              size=\"sm\"\n              class=\"text-xs\"\n              (click)=\"showNewHabitModal.set(false)\"\n            >\n              Cancel\n            </button>\n            <button ui-button size=\"sm\" class=\"text-xs\" [disabled]=\"!newHabitTitle().trim()\" (click)=\"handleAddHabit()\">\n              Add Routine\n            </button>\n          </div>\n        </div>\n      }\n\n      <!-- 4 Metric Cards -->\n      <div class=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n        <div ui-card class=\"border-border/80 bg-card text-card-foreground shadow-xs\">\n          <div ui-card-header class=\"p-4 pb-2\">\n            <div class=\"flex items-center justify-between\">\n              <span class=\"text-muted-foreground text-xs font-medium\">Current Streak</span>\n              <div\n                class=\"border-warning/25 bg-warning/10 text-warning flex size-7 items-center justify-center rounded-md border\"\n              >\n                <lucide-icon [img]=\"Flame\" class=\"fill-warning text-warning size-4\" />\n              </div>\n            </div>\n          </div>\n          <div ui-card-content class=\"space-y-1 p-4 pt-1\">\n            <div class=\"flex items-baseline gap-2\">\n              <span class=\"text-foreground font-mono text-2xl font-bold tracking-tight tabular-nums\"\n                >{{ initialStreak() }} Days</span\n              >\n              <span ui-badge variant=\"warning\" class=\"text-xs font-medium\">Active</span>\n            </div>\n            <p class=\"text-muted-foreground text-xs\">Active since Jul 29 · On track today</p>\n          </div>\n        </div>\n        <div ui-card class=\"border-border/80 bg-card text-card-foreground shadow-xs\">\n          <div ui-card-header class=\"p-4 pb-2\">\n            <div class=\"flex items-center justify-between\">\n              <span class=\"text-muted-foreground text-xs font-medium\">Longest Streak</span>\n              <div\n                class=\"border-chart-2/25 bg-chart-2/10 text-chart-2 flex size-7 items-center justify-center rounded-md border\"\n              >\n                <lucide-icon [img]=\"Trophy\" class=\"size-4\" />\n              </div>\n            </div>\n          </div>\n          <div ui-card-content class=\"space-y-1 p-4 pt-1\">\n            <div class=\"flex items-baseline gap-2\">\n              <span class=\"text-foreground font-mono text-2xl font-bold tracking-tight tabular-nums\">{{\n                longestStreak()\n              }}</span>\n              <span ui-badge variant=\"secondary\" class=\"text-xs font-medium\">Record</span>\n            </div>\n            <p class=\"text-muted-foreground text-xs\">48 continuous days set May 14, 2026</p>\n          </div>\n        </div>\n        <div ui-card class=\"border-border/80 bg-card text-card-foreground shadow-xs\">\n          <div ui-card-header class=\"p-4 pb-2\">\n            <div class=\"flex items-center justify-between\">\n              <span class=\"text-muted-foreground text-xs font-medium\">Overall Consistency</span>\n              <div\n                class=\"border-success/25 bg-success/10 text-success flex size-7 items-center justify-center rounded-md border\"\n              >\n                <lucide-icon [img]=\"TrendingUp\" class=\"size-4\" />\n              </div>\n            </div>\n          </div>\n          <div ui-card-content class=\"space-y-1 p-4 pt-1\">\n            <div class=\"flex items-baseline gap-2\">\n              <span class=\"text-foreground font-mono text-2xl font-bold tracking-tight tabular-nums\"\n                >{{ consistencyRate() }}%</span\n              >\n              <span ui-badge variant=\"success\" class=\"text-xs font-medium\">+3.1% MoM</span>\n            </div>\n            <p class=\"text-muted-foreground text-xs\">\n              {{ heatmapData().totalCompletions }} targets completed in 52 weeks\n            </p>\n          </div>\n        </div>\n        <div ui-card class=\"border-border/80 bg-card text-card-foreground shadow-xs\">\n          <div ui-card-header class=\"p-4 pb-2\">\n            <div class=\"flex items-center justify-between\">\n              <span class=\"text-muted-foreground text-xs font-medium\">Total Habits Active</span>\n              <div\n                class=\"border-info/25 bg-info/10 text-info flex size-7 items-center justify-center rounded-md border\"\n              >\n                <lucide-icon [img]=\"Target\" class=\"size-4\" />\n              </div>\n            </div>\n          </div>\n          <div ui-card-content class=\"space-y-1 p-4 pt-1\">\n            <div class=\"flex items-baseline gap-2\">\n              <span class=\"text-foreground font-mono text-2xl font-bold tracking-tight tabular-nums\"\n                >{{ totalHabitsCount() }} Daily Habits</span\n              >\n              <span\n                ui-badge\n                [variant]=\"todayCompletedCount() === totalHabitsCount() ? 'success' : 'secondary'\"\n                class=\"text-xs font-medium\"\n                >{{ todayCompletedCount() }}/{{ totalHabitsCount() }} Done</span\n              >\n            </div>\n            <p class=\"text-muted-foreground text-xs\">{{ remainingLabel() }}</p>\n          </div>\n        </div>\n      </div>\n\n      <!-- Heatmap -->\n      <div ui-card class=\"border-border/80 bg-card text-card-foreground shadow-xs\">\n        <div ui-card-header class=\"p-4 pb-3 sm:p-5 sm:pb-3\">\n          <div class=\"flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between\">\n            <div>\n              <h3 ui-card-title class=\"text-sm font-semibold tracking-tight\">52-Week Habit Consistency Matrix</h3>\n              <p ui-card-description class=\"text-xs\">\n                Annual contribution heatmap across all daily registered routines.\n              </p>\n            </div>\n            <div class=\"text-muted-foreground flex items-center gap-2 text-xs\">\n              @if (hoveredCell(); as hovered) {\n                <div\n                  class=\"text-foreground bg-muted/60 border-border flex items-center gap-1.5 rounded-md border px-2 py-1 font-medium\"\n                >\n                  <span class=\"bg-success size-2 rounded-full\"></span>\n                  <span>{{ hovered.formattedDate }} · </span>\n                  <span class=\"font-mono font-semibold\">{{ hovered.count }}/5 habits</span>\n                </div>\n              } @else {\n                <div class=\"flex items-center gap-1.5\">\n                  <lucide-icon [img]=\"CheckCircle2\" class=\"text-success size-3.5\" />\n                  <span class=\"text-foreground font-mono font-medium tabular-nums\">{{\n                    heatmapData().totalCompletions\n                  }}</span>\n                  <span>completions logged</span>\n                </div>\n              }\n            </div>\n          </div>\n        </div>\n        <div ui-card-content class=\"space-y-4 p-4 pt-1 sm:p-5 sm:pt-1\">\n          <div class=\"overflow-x-auto pb-2\">\n            <div class=\"max-w-[740px] min-w-full space-y-1.5\">\n              <div class=\"text-muted-foreground flex pl-8 text-xs font-medium select-none\">\n                @for (label of heatmapData().monthLabels; track label.name + '-' + label.weekIndex) {\n                  <div class=\"truncate\" style=\"width: 59.8px\">{{ label.name }}</div>\n                }\n              </div>\n              <div class=\"flex gap-2\">\n                <div\n                  class=\"text-muted-foreground flex w-6 shrink-0 flex-col justify-between py-[1px] text-xs font-medium select-none\"\n                >\n                  <span>Mon</span><span class=\"opacity-0\">Tue</span><span>Wed</span><span class=\"opacity-0\">Thu</span\n                  ><span>Fri</span><span class=\"opacity-0\">Sat</span><span>Sun</span>\n                </div>\n                <div class=\"flex flex-1 gap-[3px]\">\n                  @for (week of heatmapData().weeks; track $index) {\n                    <div class=\"flex flex-1 flex-col gap-[3px]\">\n                      @for (cell of week; track cell.date) {\n                        <button\n                          type=\"button\"\n                          [attr.aria-label]=\"cell.formattedDate + ': ' + cell.count + ' habits completed'\"\n                          [attr.title]=\"cell.formattedDate + ': ' + cell.count + ' habits completed'\"\n                          [class]=\"heatmapCellClass(cell.level)\"\n                          (mouseenter)=\"hoveredCell.set(cell)\"\n                          (mouseleave)=\"hoveredCell.set(null)\"\n                          (focus)=\"hoveredCell.set(cell)\"\n                          (blur)=\"hoveredCell.set(null)\"\n                        ></button>\n                      }\n                    </div>\n                  }\n                </div>\n              </div>\n            </div>\n          </div>\n          <div\n            class=\"border-border/50 text-muted-foreground flex flex-col gap-2 border-t pt-3 text-xs sm:flex-row sm:items-center sm:justify-between\"\n          >\n            <div class=\"flex items-center gap-1.5\">\n              <span class=\"text-foreground font-mono font-semibold tabular-nums\">94.2%</span>\n              <span>consistency baseline · 52 continuous weeks tracked</span>\n            </div>\n            <div class=\"flex items-center gap-2 self-end sm:self-auto\">\n              <span>Less</span>\n              <div class=\"flex gap-1\">\n                <span\n                  class=\"bg-muted/60 dark:bg-muted/30 border-border/40 size-3 rounded-[2px] border\"\n                  title=\"0 habits\"\n                ></span>\n                <span class=\"border-success/20 bg-success/25 size-3 rounded-[2px] border\" title=\"1-2 habits\"></span>\n                <span class=\"border-success/30 bg-success/50 size-3 rounded-[2px] border\" title=\"3 habits\"></span>\n                <span class=\"border-success/40 bg-success/75 size-3 rounded-[2px] border\" title=\"4 habits\"></span>\n                <span class=\"border-success/60 bg-success size-3 rounded-[2px] border\" title=\"5 habits\"></span>\n              </div>\n              <span>More</span>\n            </div>\n          </div>\n        </div>\n      </div>\n\n      <!-- Checklist -->\n      <div ui-card class=\"border-border/80 bg-card text-card-foreground shadow-xs\">\n        <div ui-card-header class=\"p-4 pb-3 sm:p-5 sm:pb-3\">\n          <div class=\"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n            <div class=\"space-y-0.5\">\n              <h3 ui-card-title class=\"text-sm font-semibold tracking-tight\">Today's Habit Checklist</h3>\n              <p ui-card-description class=\"text-xs\">\n                Interactive 7-day strip. Mark today or previous days to maintain your active flame streaks.\n              </p>\n            </div>\n            <div class=\"flex flex-wrap items-center gap-1.5\">\n              @for (cat of categories(); track cat) {\n                <button\n                  ui-button\n                  [variant]=\"activeCategory() === cat ? 'default' : 'ghost'\"\n                  size=\"sm\"\n                  class=\"h-7 px-2.5 text-xs font-medium\"\n                  (click)=\"activeCategory.set(cat)\"\n                >\n                  {{ cat }}\n                </button>\n              }\n            </div>\n          </div>\n        </div>\n        <div ui-card-content class=\"p-0\">\n          <div class=\"divide-border/60 divide-y\">\n            @for (habit of filteredHabits(); track habit.id) {\n              <div\n                class=\"hover:bg-muted/20 flex flex-col gap-4 p-4 transition-colors sm:p-5 lg:flex-row lg:items-center lg:justify-between\"\n              >\n                <div class=\"flex items-start gap-3 lg:w-1/3\">\n                  <div\n                    class=\"bg-muted text-muted-foreground border-border/60 flex size-9 shrink-0 items-center justify-center rounded-lg border shadow-xs\"\n                  >\n                    <lucide-icon [img]=\"habitIcon(habit.icon)\" class=\"size-4.5\" />\n                  </div>\n                  <div class=\"space-y-1\">\n                    <div class=\"flex flex-wrap items-center gap-2\">\n                      <p class=\"text-foreground text-sm leading-none font-semibold\">{{ habit.title }}</p>\n                      <span ui-badge [class]=\"categoryBadgeClass(habit.category) + ' border text-xs font-medium'\">{{\n                        habit.category\n                      }}</span>\n                    </div>\n                    <p class=\"text-muted-foreground text-xs\">{{ habit.description }}</p>\n                  </div>\n                </div>\n                <div class=\"flex items-center gap-2 lg:w-28\">\n                  <div\n                    class=\"border-warning/30 bg-warning/10 text-warning flex size-6.5 shrink-0 items-center justify-center rounded-md border\"\n                  >\n                    <lucide-icon [img]=\"Flame\" class=\"fill-warning text-warning size-3.5\" />\n                  </div>\n                  <div>\n                    <p class=\"text-foreground font-mono text-xs font-bold tabular-nums\">\n                      {{ habit.streakDays }}d streak\n                    </p>\n                    <p class=\"text-muted-foreground text-xs\">unbroken</p>\n                  </div>\n                </div>\n                <div class=\"space-y-1 lg:w-56\">\n                  <div class=\"text-muted-foreground flex items-center justify-between px-0.5 text-xs font-medium\">\n                    @for (day of weekDays; track day.short; let dIdx = $index) {\n                      <span [class]=\"dayLabelClass(dIdx)\">{{ day.short[0] }}</span>\n                    }\n                  </div>\n                  <div class=\"flex items-center justify-between gap-1\">\n                    @for (day of weekDays; track day.short; let dIdx = $index) {\n                      <button\n                        type=\"button\"\n                        [attr.aria-label]=\"\n                          habit.title + ' on ' + day.short + ': ' + (habit.weekHistory[dIdx] ? 'Completed' : 'Incomplete')\n                        \"\n                        [class]=\"dayCellClass(habit, dIdx)\"\n                        (click)=\"toggleDay(habit.id, dIdx)\"\n                      >\n                        @if (habit.weekHistory[dIdx]) {\n                          <lucide-icon [img]=\"Check\" class=\"size-3.5 stroke-[2.5]\" />\n                        } @else {\n                          <span class=\"text-xs\">{{ day.num }}</span>\n                        }\n                      </button>\n                    }\n                  </div>\n                </div>\n                <div class=\"space-y-1.5 lg:w-36\">\n                  <div class=\"flex items-center justify-between text-xs\">\n                    <span class=\"text-muted-foreground\">Rate</span>\n                    <span class=\"text-foreground font-mono font-semibold tabular-nums\"\n                      >{{ habit.completionRate }}%</span\n                    >\n                  </div>\n                  <ui-progress [value]=\"habit.completionRate\" class=\"h-1.5\" />\n                </div>\n                <div class=\"flex items-center justify-end lg:w-32\">\n                  @if (habit.weekHistory[6]) {\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 h-8 w-full gap-1.5 text-xs font-medium shadow-xs sm:w-auto\"\n                      (click)=\"toggleToday(habit.id)\"\n                    >\n                      <lucide-icon [img]=\"CheckCircle2\" class=\"text-success size-3.5\" />\n                      <span>Done Today</span>\n                    </button>\n                  } @else {\n                    <button\n                      ui-button\n                      variant=\"default\"\n                      size=\"sm\"\n                      class=\"h-8 w-full gap-1.5 text-xs font-medium shadow-xs sm:w-auto\"\n                      (click)=\"toggleToday(habit.id)\"\n                    >\n                      <lucide-icon [img]=\"Check\" class=\"size-3.5\" />\n                      <span>Check Today</span>\n                    </button>\n                  }\n                </div>\n              </div>\n            }\n          </div>\n        </div>\n      </div>\n    </div>\n  `,\n})\nexport class UiHabitStreakTrackerComponent {\n  protected readonly BookOpen = BookOpen\n  protected readonly Calendar = Calendar\n  protected readonly Check = Check\n  protected readonly CheckCircle2 = CheckCircle2\n  protected readonly Flame = Flame\n  protected readonly Plus = Plus\n  protected readonly Target = Target\n  protected readonly TrendingUp = TrendingUp\n  protected readonly Trophy = Trophy\n  protected readonly X = X\n\n  protected readonly weekDays = weekDayColumns\n\n  @Input('initialStreak') set initialStreakInput(v: number) {\n    if (v !== undefined && v !== null) this.initialStreak.set(v)\n  }\n  @Input('longestStreak') set longestStreakInput(v: string) {\n    if (v !== undefined) this.longestStreak.set(v)\n  }\n  @Input('consistencyRate') set consistencyRateInput(v: number) {\n    if (v !== undefined && v !== null) this.consistencyRate.set(v)\n  }\n  @Input('activeHabitsCount') set activeHabitsCountInput(v: number) {\n    if (v !== undefined && v !== null) this.activeHabitsCount.set(v)\n  }\n  @Input('monthTitle') set monthTitleInput(v: string) {\n    if (v !== undefined) this.monthTitle.set(v)\n  }\n  @Input() set habits(v: HabitItem[] | undefined) {\n    if (v) this.habitsList.set(v.map((h) => ({ ...h, weekHistory: [...h.weekHistory] })))\n  }\n  @Input('class') className?: string\n\n  readonly initialStreak = signal(24)\n  readonly longestStreak = signal('48 Days in Q2')\n  readonly consistencyRate = signal(94.2)\n  readonly activeHabitsCount = signal(5)\n  readonly monthTitle = signal('August 2026')\n  readonly habitsList = signal<HabitItem[]>(defaultHabits.map((h) => ({ ...h, weekHistory: [...h.weekHistory] })))\n\n  readonly activeCategory = signal('All')\n  readonly showNewHabitModal = signal(false)\n  readonly hoveredCell = signal<DayHeatmapCell | null>(null)\n  readonly newHabitTitle = signal('')\n  readonly newHabitDescription = signal('')\n  readonly newHabitCategory = signal<HabitCategory>('Engineering')\n  readonly newHabitIcon = signal('code')\n\n  readonly heatmapData = computed(() => generate52WeekHeatmap(this.initialStreak()))\n  readonly categories = computed(() => {\n    const cats = ['All']\n    for (const h of this.habitsList()) if (!cats.includes(h.category)) cats.push(h.category)\n    return cats\n  })\n  readonly filteredHabits = computed(() => {\n    if (this.activeCategory() === 'All') return this.habitsList()\n    return this.habitsList().filter((h) => h.category === this.activeCategory())\n  })\n  readonly todayCompletedCount = computed(() => this.habitsList().filter((h) => h.weekHistory[6]).length)\n  readonly totalHabitsCount = computed(() => this.habitsList().length)\n  readonly remainingLabel = computed(() => {\n    const done = this.todayCompletedCount()\n    const total = this.totalHabitsCount()\n    return done === total ? 'All daily targets logged!' : `${total - done} remaining for today`\n  })\n\n  get rootClass(): string {\n    return cn('w-full space-y-6', this.className)\n  }\n\n  habitIcon(icon: string): LucideIconData {\n    switch (icon) {\n      case 'code':\n        return CodeXml\n      case 'terminal':\n        return Terminal\n      case 'steps':\n        return Footprints\n      case 'book':\n        return BookOpen\n      case 'water':\n        return Droplets\n      default:\n        return Target\n    }\n  }\n\n  categoryBadgeClass(category: HabitCategory): string {\n    switch (category) {\n      case 'Engineering':\n        return 'bg-info/10 text-info border-info/25'\n      case 'Health':\n        return 'bg-success/10 text-success border-success/25'\n      case 'Learning':\n        return 'bg-chart-1/10 text-chart-1 border-chart-1/25'\n      case 'Mindset':\n        return 'bg-warning/10 text-warning border-warning/25'\n      default:\n        return 'bg-muted text-muted-foreground border-border'\n    }\n  }\n\n  heatmapCellClass(level: number): string {\n    return cn(\n      'size-3 cursor-pointer rounded-[2px] transition-colors duration-150 sm:size-3.5',\n      levelClassMap[level] ?? levelClassMap[0],\n    )\n  }\n\n  dayLabelClass(dIdx: number): string {\n    return cn('w-6 text-center', dIdx === 6 && 'text-foreground font-semibold')\n  }\n\n  dayCellClass(habit: HabitItem, dIdx: number): string {\n    return cn(\n      'focus-visible:ring-ring flex size-6 cursor-pointer items-center justify-center rounded-md text-xs transition-colors focus-visible:ring-2 focus-visible:outline-none',\n      habit.weekHistory[dIdx]\n        ? 'bg-success hover:bg-success text-white shadow-xs'\n        : 'border-border/80 bg-muted/40 text-muted-foreground/30 hover:bg-muted/80 hover:text-muted-foreground border',\n      dIdx === 6 && !habit.weekHistory[dIdx] && 'border-warning/50 bg-warning/5',\n    )\n  }\n\n  toggleModal(): void {\n    this.showNewHabitModal.set(!this.showNewHabitModal())\n  }\n\n  toggleDay(habitId: string, dayIndex: number): void {\n    this.habitsList.update((prev) =>\n      prev.map((habit) => {\n        if (habit.id !== habitId) return habit\n        const nextWeekHistory = [...habit.weekHistory]\n        nextWeekHistory[dayIndex] = !nextWeekHistory[dayIndex]\n        const completedInWeek = nextWeekHistory.filter(Boolean).length\n        const completionRate = Math.min(100, Math.round((completedInWeek / 7) * 100))\n        let streakDays = habit.streakDays\n        if (dayIndex === 6) streakDays = nextWeekHistory[6] ? streakDays + 1 : Math.max(0, streakDays - 1)\n        return { ...habit, weekHistory: nextWeekHistory, completionRate, streakDays }\n      }),\n    )\n  }\n\n  toggleToday(habitId: string): void {\n    this.toggleDay(habitId, 6)\n  }\n\n  handleAddHabit(): void {\n    if (!this.newHabitTitle().trim()) return\n    const newHabit: HabitItem = {\n      id: `habit-${Date.now()}`,\n      title: this.newHabitTitle().trim(),\n      description: this.newHabitDescription().trim() || 'Daily tracked routine',\n      category: this.newHabitCategory(),\n      icon: this.newHabitIcon(),\n      streakDays: 1,\n      completionRate: 100,\n      weekHistory: [false, false, false, false, false, false, true],\n    }\n    this.habitsList.update((prev) => [...prev, newHabit])\n    this.newHabitTitle.set('')\n    this.newHabitDescription.set('')\n    this.showNewHabitModal.set(false)\n  }\n}\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/habit-streak-tracker/habit-streak-tracker.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/habit-streak-tracker/index.ts",
      "content": "export {\n  UiHabitStreakTrackerComponent,\n  generate52WeekHeatmap,\n  type HabitItem,\n  type HabitCategory,\n  type DayHeatmapCell,\n  type MonthLabel,\n} from './habit-streak-tracker.component'\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/habit-streak-tracker/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"
  ],
  "description": "GitHub style 52-week contribution habit heatmap, flame streak counter, consistency metrics, and daily completion tracker with 7-day mini checkbox strip and progress bars.",
  "categories": [
    "productivity",
    "dashboard",
    "app"
  ]
}