{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "password-generator-widget",
  "title": "Password Generator Widget",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-angular/blocks/password-generator-widget/password-generator-widget.component.ts",
      "content": "// 1Password / Bitwarden style customizable password, Diceware passphrase and PIN\n// generator: cryptographic output with syntax highlighting, entropy strength meter with\n// crack-time estimate, per-mode configuration tabs (length slider + presets, character\n// pools, separator grid, PIN rules), and a volatile masked history drawer. Port of the\n// React block 1:1. Option changes regenerate quietly (no history spam); Regenerate logs.\nimport { ChangeDetectionStrategy, Component, Input, OnInit, computed, signal } from '@angular/core'\nimport {\n  Check,\n  Clock,\n  Copy,\n  Dice5,\n  Eye,\n  EyeOff,\n  Hash,\n  History,\n  KeyRound,\n  Lock,\n  LucideAngularModule,\n  RefreshCw,\n  Shield,\n  ShieldCheck,\n  Sliders,\n  Trash2,\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  UiCardFooterComponent,\n  UiCardHeaderComponent,\n  UiCardTitleComponent,\n} from '@/ui/card/card.component'\nimport { UiSeparatorComponent } from '@/ui/separator/separator.component'\nimport { UiSliderComponent } from '@/ui/slider/slider.component'\nimport { UiSwitchComponent } from '@/ui/switch/switch.component'\nimport {\n  UiTabsComponent,\n  UiTabsContentComponent,\n  UiTabsListComponent,\n  UiTabsTriggerComponent,\n} from '@/ui/tabs/tabs.component'\n\nexport type GeneratorMode = 'password' | 'passphrase' | 'pin'\n\nexport interface HistoryItem {\n  id: string\n  secret: string\n  mode: GeneratorMode\n  entropy: number\n  strengthLabel: string\n  createdAt: string\n}\n\nconst WORD_LIST = [\n  'anchor',\n  'apple',\n  'apron',\n  'arctic',\n  'arrow',\n  'atlas',\n  'autumn',\n  'bacon',\n  'badge',\n  'banner',\n  'beacon',\n  'blanket',\n  'breeze',\n  'bridge',\n  'cabin',\n  'cactus',\n  'camera',\n  'candle',\n  'canyon',\n  'carpet',\n  'castle',\n  'cedar',\n  'celestial',\n  'channel',\n  'cherry',\n  'chimney',\n  'cipher',\n  'circle',\n  'clay',\n  'cliff',\n  'clock',\n  'cloud',\n  'clover',\n  'cobalt',\n  'coffee',\n  'comet',\n  'compass',\n  'copper',\n  'coral',\n  'cosmos',\n  'crater',\n  'creek',\n  'crystal',\n  'curtain',\n  'delta',\n  'desert',\n  'diamond',\n  'dolphin',\n  'dragon',\n  'drift',\n  'eagle',\n  'earth',\n  'echo',\n  'eclipse',\n  'ember',\n  'emerald',\n  'engine',\n  'falcon',\n  'feather',\n  'fender',\n  'filter',\n  'flame',\n  'flask',\n  'forest',\n  'fossil',\n  'galaxy',\n  'garden',\n  'garlic',\n  'garnet',\n  'gem',\n  'glacier',\n  'glass',\n  'globe',\n  'granite',\n  'gravel',\n  'grove',\n  'harbor',\n  'haven',\n  'hawk',\n  'hazel',\n  'horizon',\n  'island',\n  'ivory',\n  'jungle',\n  'jupiter',\n  'kettle',\n  'lagoon',\n  'lantern',\n  'laser',\n  'lava',\n  'leaf',\n  'lemon',\n  'leopard',\n  'lighthouse',\n  'lightning',\n  'lotus',\n  'lunar',\n  'magnet',\n  'mango',\n  'maple',\n  'marble',\n  'matrix',\n  'meadow',\n  'meteor',\n  'mineral',\n  'mirror',\n  'monarch',\n  'moon',\n  'mosaic',\n  'mountain',\n  'nebula',\n  'nickel',\n  'oasis',\n  'ocean',\n  'olive',\n  'onyx',\n  'opal',\n  'orbit',\n  'orchid',\n  'oxygen',\n  'palace',\n  'panther',\n  'paradise',\n  'pebble',\n  'pelican',\n  'peacock',\n  'phoenix',\n  'pillar',\n  'planet',\n  'plasma',\n  'platinum',\n  'polar',\n  'prism',\n  'pulse',\n  'pyramid',\n  'quartz',\n  'quiver',\n  'radar',\n  'rainbow',\n  'raven',\n  'reef',\n  'ridge',\n  'ripple',\n  'river',\n  'rocket',\n  'ruby',\n  'safari',\n  'sail',\n  'sapphire',\n  'saturn',\n  'shadow',\n  'shield',\n  'sierra',\n  'silver',\n  'solstice',\n  'spark',\n  'sphere',\n  'spiral',\n  'spring',\n  'spruce',\n  'star',\n  'stone',\n  'summit',\n  'sunset',\n  'surge',\n  'timber',\n  'topaz',\n  'torch',\n  'trail',\n  'treasure',\n  'tundra',\n  'valley',\n  'vector',\n  'velvet',\n  'vessel',\n  'vintage',\n  'violet',\n  'vortex',\n  'voyage',\n  'vulcan',\n  'walnut',\n  'willow',\n  'wind',\n  'winter',\n  'zenith',\n  'zephyr',\n]\n\nconst UPPERCASE_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'\nconst LOWERCASE_CHARS = 'abcdefghijklmnopqrstuvwxyz'\nconst NUMBER_CHARS = '0123456789'\nconst SYMBOL_CHARS = '!@#$%^&*()_+-=[]{}|;:,.<>?'\nconst AMBIGUOUS_CHARS = new Set(['l', '1', 'I', 'O', '0', 'o', '|', '`', \"'\", '\"', ',', ';'])\n\nconst PASSWORD_PRESETS = [12, 16, 20, 24, 32, 48, 64]\nconst WORD_PRESETS = [3, 4, 5, 6, 8]\nconst PIN_PRESETS = [4, 6, 8, 10, 12, 16]\n\nconst SEPARATORS = [\n  { id: '-', label: 'Hyphen (-)' },\n  { id: '_', label: 'Under (_)' },\n  { id: '.', label: 'Period (.)' },\n  { id: ' ', label: 'Space ( )' },\n  { id: '/', label: 'Slash (/)' },\n]\n\nexport interface FormattedChar {\n  char: string\n  type: 'number' | 'symbol' | 'letter' | 'separator'\n}\n\nfunction classifyChar(char: string): FormattedChar['type'] {\n  if (/[0-9]/.test(char)) return 'number'\n  if (/[-_. /]/.test(char)) return 'separator'\n  if (/[^a-zA-Z0-9]/.test(char)) return 'symbol'\n  return 'letter'\n}\n\nfunction getRandomInt(max: number): number {\n  if (typeof window !== 'undefined' && window.crypto && window.crypto.getRandomValues) {\n    const array = new Uint32Array(1)\n    window.crypto.getRandomValues(array)\n    return array[0]! % max\n  }\n  return Math.floor(Math.random() * max)\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-password-generator-widget, [ui-password-generator-widget]',\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    UiCardFooterComponent,\n    UiCardHeaderComponent,\n    UiCardTitleComponent,\n    UiSeparatorComponent,\n    UiSliderComponent,\n    UiSwitchComponent,\n    UiTabsComponent,\n    UiTabsContentComponent,\n    UiTabsListComponent,\n    UiTabsTriggerComponent,\n  ],\n  template: `\n    <div data-slot=\"password-generator-widget\" [class]=\"rootClass\">\n      <!-- HERO: GENERATOR OUTPUT CARD -->\n      <div ui-card class=\"overflow-hidden shadow-xs\">\n        <div ui-card-header class=\"border-border/50 bg-muted/20 border-b pb-3\">\n          <div class=\"flex flex-wrap items-center justify-between gap-2\">\n            <div class=\"flex items-center gap-2\">\n              <div class=\"bg-primary/10 text-primary flex size-8 items-center justify-center rounded-lg\">\n                <lucide-icon [img]=\"KeyRound\" class=\"size-4\" />\n              </div>\n              <div>\n                <h3 ui-card-title class=\"text-base font-semibold\">Generated Secret</h3>\n                <p ui-card-description class=\"text-xs\">Client-side cryptographic output &amp; entropy estimation</p>\n              </div>\n            </div>\n            <div class=\"flex items-center gap-2\">\n              <span ui-badge variant=\"outline\" class=\"gap-1.5 text-xs font-normal\">\n                <span class=\"bg-success size-2 animate-pulse rounded-full\"></span>\n                100% Client-Side\n              </span>\n            </div>\n          </div>\n        </div>\n\n        <div ui-card-content class=\"space-y-6 pt-6\">\n          <div\n            class=\"border-border/80 bg-muted/40 dark:bg-muted/20 relative flex min-h-[4.75rem] w-full flex-col justify-center gap-4 rounded-xl border p-4 sm:flex-row sm:items-center sm:justify-between\"\n          >\n            <div\n              class=\"flex flex-wrap items-center gap-x-0.5 gap-y-1 pr-2 font-mono text-lg font-medium tracking-wider break-all select-all md:text-xl\"\n            >\n              @if (formattedCharacters().length > 0) {\n                @for (item of formattedCharacters(); track $index) {\n                  <span [class]=\"charClass(item.type)\">{{ item.char }}</span>\n                }\n              } @else {\n                <span class=\"text-muted-foreground text-sm\">Generating secret...</span>\n              }\n            </div>\n\n            <div class=\"flex shrink-0 items-center gap-2 self-end sm:self-center\">\n              <button\n                ui-button\n                variant=\"outline\"\n                size=\"sm\"\n                class=\"h-9 gap-1.5 px-3 active:scale-95\"\n                (click)=\"handleManualRegenerate()\"\n              >\n                <lucide-icon [img]=\"RefreshCw\" [class]=\"spinClass\" />\n                <span class=\"text-xs font-medium\">Regenerate</span>\n              </button>\n\n              <button ui-button variant=\"default\" size=\"sm\" [class]=\"copyClass\" (click)=\"copySecret(currentSecret())\">\n                @if (copied()) {\n                  <lucide-icon [img]=\"Check\" class=\"size-4 stroke-[2.5]\" />\n                } @else {\n                  <lucide-icon [img]=\"Copy\" class=\"size-4\" />\n                }\n                <span class=\"text-xs font-medium\">{{ copied() ? 'Copied!' : 'Copy' }}</span>\n              </button>\n            </div>\n          </div>\n\n          <!-- Entropy & Strength Meter -->\n          <div class=\"border-border/60 bg-muted/10 space-y-2.5 rounded-lg border p-4\">\n            <div class=\"flex flex-wrap items-center justify-between gap-2 text-xs\">\n              <div class=\"flex items-center gap-2\">\n                <lucide-icon [img]=\"ShieldCheck\" class=\"size-4\" [class]=\"currentStrength().color\" />\n                <span class=\"text-foreground font-semibold\">{{ currentStrength().label }}</span>\n                <span class=\"text-muted-foreground font-mono\">{{ currentStrength().entropy }}-bit entropy</span>\n              </div>\n              <div class=\"text-muted-foreground flex items-center gap-1.5\">\n                <lucide-icon [img]=\"Clock\" class=\"size-3.5\" />\n                <span>Crack time:</span>\n                <span class=\"text-foreground font-medium\">{{ currentStrength().crackTime }}</span>\n              </div>\n            </div>\n\n            <div class=\"grid h-2 w-full grid-cols-4 gap-1.5 overflow-hidden rounded-full\">\n              <div class=\"rounded-full transition-colors duration-300\" [class]=\"segClass(1)\"></div>\n              <div class=\"rounded-full transition-colors duration-300\" [class]=\"segClass(2)\"></div>\n              <div class=\"rounded-full transition-colors duration-300\" [class]=\"segClass(3)\"></div>\n              <div class=\"rounded-full transition-colors duration-300\" [class]=\"segClass(4)\"></div>\n            </div>\n\n            <div class=\"text-muted-foreground flex flex-wrap items-center gap-4 pt-1 text-xs\">\n              <span class=\"flex items-center gap-1.5\">\n                <span class=\"bg-foreground size-2 rounded-full\"></span>\n                Letters\n              </span>\n              <span class=\"flex items-center gap-1.5\">\n                <span class=\"bg-info size-2 rounded-full\"></span>\n                Numbers (0-9)\n              </span>\n              <span class=\"flex items-center gap-1.5\">\n                <span class=\"bg-warning size-2 rounded-full\"></span>\n                Symbols (!@#$)\n              </span>\n            </div>\n          </div>\n        </div>\n      </div>\n\n      <!-- GENERATOR CONFIGURATION CARD -->\n      <div ui-card class=\"shadow-xs\">\n        <div ui-card-header class=\"border-border/50 border-b pb-3\">\n          <div class=\"flex items-center gap-2\">\n            <div class=\"bg-muted text-muted-foreground flex size-8 items-center justify-center rounded-lg\">\n              <lucide-icon [img]=\"Sliders\" class=\"size-4\" />\n            </div>\n            <div>\n              <h3 ui-card-title class=\"text-base font-semibold\">Generator Configuration</h3>\n              <p ui-card-description class=\"text-xs\">\n                Customize character pools, length constraints, and format patterns\n              </p>\n            </div>\n          </div>\n        </div>\n\n        <div ui-card-content class=\"pt-6\">\n          <ui-tabs [value]=\"mode()\" (valueChange)=\"setMode($event)\" class=\"w-full\">\n            <ui-tabs-list class=\"mb-6 grid w-full grid-cols-3\">\n              <button ui-tabs-trigger value=\"password\" class=\"gap-1.5 text-xs\">\n                <lucide-icon [img]=\"Lock\" class=\"size-3.5\" />\n                <span>Random Password</span>\n              </button>\n              <button ui-tabs-trigger value=\"passphrase\" class=\"gap-1.5 text-xs\">\n                <lucide-icon [img]=\"Dice5\" class=\"size-3.5\" />\n                <span>Passphrase</span>\n              </button>\n              <button ui-tabs-trigger value=\"pin\" class=\"gap-1.5 text-xs\">\n                <lucide-icon [img]=\"Hash\" class=\"size-3.5\" />\n                <span>PIN Code</span>\n              </button>\n            </ui-tabs-list>\n\n            <!-- TAB 1: RANDOM PASSWORD -->\n            <ui-tabs-content value=\"password\" class=\"mt-0 space-y-6\">\n              <div class=\"space-y-3\">\n                <div class=\"flex items-center justify-between\">\n                  <label class=\"text-foreground text-sm font-medium\">Password Length</label>\n                  <span ui-badge variant=\"secondary\" class=\"font-mono text-xs font-semibold tabular-nums\">\n                    {{ passwordLength() }} characters\n                  </span>\n                </div>\n\n                <ui-slider\n                  [value]=\"[passwordLength()]\"\n                  [min]=\"8\"\n                  [max]=\"64\"\n                  [step]=\"1\"\n                  class=\"w-full py-1\"\n                  (valueChange)=\"setPasswordLength($event[0] ?? passwordLength())\"\n                />\n\n                <div class=\"flex flex-wrap items-center gap-1.5 pt-1\">\n                  <span class=\"text-muted-foreground mr-1 text-xs\">Presets:</span>\n                  @for (preset of passwordPresets; track preset) {\n                    <button\n                      type=\"button\"\n                      [class]=\"presetClass(passwordLength() === preset)\"\n                      (click)=\"setPasswordLength(preset)\"\n                    >\n                      {{ preset }}\n                    </button>\n                  }\n                </div>\n              </div>\n\n              <ui-separator />\n\n              <div class=\"space-y-3\">\n                <label class=\"text-muted-foreground text-xs font-medium\"> Character Pools </label>\n\n                <div class=\"border-border/80 bg-muted/20 space-y-3 rounded-lg border p-4\">\n                  <div class=\"flex items-center justify-between\">\n                    <div class=\"space-y-0.5\">\n                      <div class=\"text-foreground flex items-center gap-2 text-sm font-medium\">\n                        <span>Uppercase Letters</span>\n                        <span class=\"text-muted-foreground font-mono text-xs font-normal\">(A-Z)</span>\n                      </div>\n                      <p class=\"text-muted-foreground text-xs\">Include capital Latin characters</p>\n                    </div>\n                    <ui-switch [checked]=\"includeUpper()\" (checkedChange)=\"toggleUpper($event)\" />\n                  </div>\n\n                  <ui-separator />\n\n                  <div class=\"flex items-center justify-between\">\n                    <div class=\"space-y-0.5\">\n                      <div class=\"text-foreground flex items-center gap-2 text-sm font-medium\">\n                        <span>Lowercase Letters</span>\n                        <span class=\"text-muted-foreground font-mono text-xs font-normal\">(a-z)</span>\n                      </div>\n                      <p class=\"text-muted-foreground text-xs\">Include small Latin characters</p>\n                    </div>\n                    <ui-switch [checked]=\"includeLower()\" (checkedChange)=\"toggleLower($event)\" />\n                  </div>\n\n                  <ui-separator />\n\n                  <div class=\"flex items-center justify-between\">\n                    <div class=\"space-y-0.5\">\n                      <div class=\"text-foreground flex items-center gap-2 text-sm font-medium\">\n                        <span>Numbers</span>\n                        <span class=\"text-info font-mono text-xs font-semibold\">(0-9)</span>\n                      </div>\n                      <p class=\"text-muted-foreground text-xs\">Include numeric digits 0 through 9</p>\n                    </div>\n                    <ui-switch [checked]=\"includeNumbers()\" (checkedChange)=\"toggleNumbers($event)\" />\n                  </div>\n\n                  <ui-separator />\n\n                  <div class=\"flex items-center justify-between\">\n                    <div class=\"space-y-0.5\">\n                      <div class=\"text-foreground flex items-center gap-2 text-sm font-medium\">\n                        <span>Special Symbols</span>\n                        <span class=\"text-warning font-mono text-xs font-semibold\">(!@#$%^&*)</span>\n                      </div>\n                      <p class=\"text-muted-foreground text-xs\">Include non-alphanumeric punctuation marks</p>\n                    </div>\n                    <ui-switch [checked]=\"includeSymbols()\" (checkedChange)=\"toggleSymbols($event)\" />\n                  </div>\n\n                  <ui-separator />\n\n                  <div class=\"flex items-center justify-between\">\n                    <div class=\"space-y-0.5\">\n                      <div class=\"text-foreground flex items-center gap-2 text-sm font-medium\">\n                        <span>Avoid Ambiguous Characters</span>\n                        <span class=\"text-muted-foreground font-mono text-xs font-normal\">(l, 1, I, O, 0)</span>\n                      </div>\n                      <p class=\"text-muted-foreground text-xs\">Exclude confusing lookalike letters and digits</p>\n                    </div>\n                    <ui-switch [checked]=\"avoidAmbiguous()\" (checkedChange)=\"setAvoidAmbiguous($event)\" />\n                  </div>\n                </div>\n              </div>\n            </ui-tabs-content>\n\n            <!-- TAB 2: MEMORABLE PASSPHRASE -->\n            <ui-tabs-content value=\"passphrase\" class=\"mt-0 space-y-6\">\n              <div class=\"space-y-3\">\n                <div class=\"flex items-center justify-between\">\n                  <label class=\"text-foreground text-sm font-medium\">Number of Words</label>\n                  <span ui-badge variant=\"secondary\" class=\"font-mono text-xs font-semibold tabular-nums\">\n                    {{ wordCount() }} words\n                  </span>\n                </div>\n\n                <ui-slider\n                  [value]=\"[wordCount()]\"\n                  [min]=\"3\"\n                  [max]=\"8\"\n                  [step]=\"1\"\n                  class=\"w-full py-1\"\n                  (valueChange)=\"setWordCount($event[0] ?? wordCount())\"\n                />\n\n                <div class=\"flex flex-wrap items-center gap-1.5 pt-1\">\n                  <span class=\"text-muted-foreground mr-1 text-xs\">Presets:</span>\n                  @for (preset of wordPresets; track preset) {\n                    <button type=\"button\" [class]=\"presetClass(wordCount() === preset)\" (click)=\"setWordCount(preset)\">\n                      {{ preset }} words\n                    </button>\n                  }\n                </div>\n              </div>\n\n              <ui-separator />\n\n              <div class=\"space-y-3\">\n                <label class=\"text-muted-foreground text-xs font-medium\"> Word Separator </label>\n\n                <div class=\"grid grid-cols-5 gap-2\">\n                  @for (sep of separators; track sep.id) {\n                    <button\n                      type=\"button\"\n                      [class]=\"separatorClass(separator() === sep.id)\"\n                      (click)=\"setSeparator(sep.id)\"\n                    >\n                      <span class=\"font-mono text-sm font-semibold\">{{ sep.id === ' ' ? '␣' : sep.id }}</span>\n                      <span class=\"mt-0.5 truncate text-xs\">{{ sepLabel(sep.label) }}</span>\n                    </button>\n                  }\n                </div>\n              </div>\n\n              <ui-separator />\n\n              <div class=\"border-border/80 bg-muted/20 space-y-3 rounded-lg border p-4\">\n                <div class=\"flex items-center justify-between\">\n                  <div class=\"space-y-0.5\">\n                    <div class=\"text-foreground text-sm font-medium\">Capitalize Words</div>\n                    <p class=\"text-muted-foreground text-xs\">Transform each word into TitleCase for easy reading</p>\n                  </div>\n                  <ui-switch [checked]=\"capitalizeWords()\" (checkedChange)=\"setCapitalizeWords($event)\" />\n                </div>\n\n                <ui-separator />\n\n                <div class=\"flex items-center justify-between\">\n                  <div class=\"space-y-0.5\">\n                    <div class=\"text-foreground text-sm font-medium\">Append Random Number</div>\n                    <p class=\"text-muted-foreground text-xs\">Add a 2-digit number (10-99) for additional security</p>\n                  </div>\n                  <ui-switch\n                    [checked]=\"includeNumberInPassphrase()\"\n                    (checkedChange)=\"setIncludeNumberInPassphrase($event)\"\n                  />\n                </div>\n              </div>\n            </ui-tabs-content>\n\n            <!-- TAB 3: PIN CODE -->\n            <ui-tabs-content value=\"pin\" class=\"mt-0 space-y-6\">\n              <div class=\"space-y-3\">\n                <div class=\"flex items-center justify-between\">\n                  <label class=\"text-foreground text-sm font-medium\">PIN Length</label>\n                  <span ui-badge variant=\"secondary\" class=\"font-mono text-xs font-semibold tabular-nums\">\n                    {{ pinLength() }} digits\n                  </span>\n                </div>\n\n                <ui-slider\n                  [value]=\"[pinLength()]\"\n                  [min]=\"4\"\n                  [max]=\"16\"\n                  [step]=\"1\"\n                  class=\"w-full py-1\"\n                  (valueChange)=\"setPinLength($event[0] ?? pinLength())\"\n                />\n\n                <div class=\"flex flex-wrap items-center gap-1.5 pt-1\">\n                  <span class=\"text-muted-foreground mr-1 text-xs\">Presets:</span>\n                  @for (preset of pinPresets; track preset) {\n                    <button type=\"button\" [class]=\"presetClass(pinLength() === preset)\" (click)=\"setPinLength(preset)\">\n                      {{ preset }} digits\n                    </button>\n                  }\n                </div>\n              </div>\n\n              <ui-separator />\n\n              <div class=\"border-border/80 bg-muted/20 space-y-3 rounded-lg border p-4\">\n                <div class=\"flex items-center justify-between\">\n                  <div class=\"space-y-0.5\">\n                    <div class=\"text-foreground text-sm font-medium\">Avoid Consecutive Repeated Digits</div>\n                    <p class=\"text-muted-foreground text-xs\">\n                      Prevent obvious repetitive patterns like &ldquo;1122&rdquo; or &ldquo;8888&rdquo;\n                    </p>\n                  </div>\n                  <ui-switch [checked]=\"avoidPinRepeats()\" (checkedChange)=\"setAvoidPinRepeats($event)\" />\n                </div>\n              </div>\n            </ui-tabs-content>\n          </ui-tabs>\n        </div>\n      </div>\n\n      <!-- RECENT HISTORY CARD -->\n      <div ui-card class=\"shadow-xs\">\n        <div ui-card-header class=\"border-border/50 border-b pb-3\">\n          <div class=\"flex flex-wrap items-center justify-between gap-2\">\n            <div class=\"flex items-center gap-2\">\n              <div class=\"bg-muted text-muted-foreground flex size-8 items-center justify-center rounded-lg\">\n                <lucide-icon [img]=\"History\" class=\"size-4\" />\n              </div>\n              <div>\n                <h3 ui-card-title class=\"text-base font-semibold\">Generated History</h3>\n                <p ui-card-description class=\"text-xs\">\n                  Stored in volatile browser memory only • Cleared on page refresh\n                </p>\n              </div>\n            </div>\n            @if (history().length > 0) {\n              <button\n                ui-button\n                variant=\"ghost\"\n                size=\"sm\"\n                class=\"text-muted-foreground hover:text-destructive h-8 gap-1.5 text-xs\"\n                (click)=\"clearHistory()\"\n              >\n                <lucide-icon [img]=\"Trash2\" class=\"size-3.5\" />\n                <span>Clear History</span>\n              </button>\n            }\n          </div>\n        </div>\n\n        <div ui-card-content class=\"pt-4\">\n          @if (history().length === 0) {\n            <div class=\"text-muted-foreground py-8 text-center text-xs\">\n              No recent secrets in session history. Regenerate or copy above to log items here.\n            </div>\n          } @else {\n            <div class=\"divide-border/60 divide-y\">\n              @for (item of history(); track item.id) {\n                <div\n                  class=\"group hover:bg-muted/30 flex flex-col justify-between gap-3 rounded-md px-1 py-3 transition-colors sm:flex-row sm:items-center\"\n                >\n                  <div class=\"flex min-w-0 items-center gap-3\">\n                    <div\n                      class=\"bg-muted text-muted-foreground flex size-7 shrink-0 items-center justify-center rounded-md\"\n                    >\n                      @if (item.mode === 'password') {\n                        <lucide-icon [img]=\"Lock\" class=\"size-3.5\" />\n                      } @else if (item.mode === 'passphrase') {\n                        <lucide-icon [img]=\"Dice5\" class=\"size-3.5\" />\n                      } @else {\n                        <lucide-icon [img]=\"Hash\" class=\"size-3.5\" />\n                      }\n                    </div>\n\n                    <div class=\"min-w-0 space-y-1\">\n                      <div class=\"flex items-center gap-2\">\n                        <div class=\"max-w-[18rem] truncate font-mono text-xs font-medium md:max-w-md\">\n                          @if (isRevealed(item.id)) {\n                            @for (sub of formatItem(item.secret); track $index) {\n                              <span [class]=\"charClass(sub.type)\">{{ sub.char }}</span>\n                            }\n                          } @else {\n                            <span class=\"text-muted-foreground tracking-widest\">••••••••••••••••</span>\n                          }\n                        </div>\n\n                        <button\n                          type=\"button\"\n                          class=\"text-muted-foreground hover:text-foreground cursor-pointer p-0.5 transition-colors\"\n                          title=\"Toggle Visibility\"\n                          (click)=\"toggleHistoryReveal(item.id)\"\n                        >\n                          @if (isRevealed(item.id)) {\n                            <lucide-icon [img]=\"EyeOff\" class=\"size-3.5\" />\n                          } @else {\n                            <lucide-icon [img]=\"Eye\" class=\"size-3.5\" />\n                          }\n                        </button>\n                      </div>\n\n                      <div class=\"text-muted-foreground flex items-center gap-2 text-xs\">\n                        <span ui-badge variant=\"outline\" class=\"px-1.5 py-0 text-xs font-normal\">\n                          {{ item.mode }}\n                        </span>\n                        <span>•</span>\n                        <span>{{ item.entropy }}-bit ({{ item.strengthLabel }})</span>\n                        <span>•</span>\n                        <span>{{ item.createdAt }}</span>\n                      </div>\n                    </div>\n                  </div>\n\n                  <div class=\"flex shrink-0 items-center gap-2 self-end sm:self-center\">\n                    <button\n                      ui-button\n                      variant=\"outline\"\n                      size=\"sm\"\n                      [class]=\"historyCopyClass(item.id)\"\n                      (click)=\"copySecret(item.secret, true, item.id)\"\n                    >\n                      @if (copiedHistoryId() === item.id) {\n                        <lucide-icon [img]=\"Check\" class=\"size-3.5\" />\n                      } @else {\n                        <lucide-icon [img]=\"Copy\" class=\"size-3.5\" />\n                      }\n                      <span>{{ copiedHistoryId() === item.id ? 'Copied' : 'Copy' }}</span>\n                    </button>\n                  </div>\n                </div>\n              }\n            </div>\n          }\n        </div>\n        <div\n          ui-card-footer\n          class=\"text-muted-foreground border-border/40 flex items-center justify-between border-t pt-3 pb-4 text-xs\"\n        >\n          <span class=\"flex items-center gap-1.5\">\n            <lucide-icon [img]=\"Shield\" class=\"text-success size-3.5\" />\n            Cryptographically random • Never transmitted over network\n          </span>\n          <span class=\"font-mono tabular-nums\">{{ history().length }} entries</span>\n        </div>\n      </div>\n    </div>\n  `,\n})\nexport class UiPasswordGeneratorWidgetComponent implements OnInit {\n  protected readonly Check = Check\n  protected readonly Clock = Clock\n  protected readonly Copy = Copy\n  protected readonly Dice5 = Dice5\n  protected readonly Eye = Eye\n  protected readonly EyeOff = EyeOff\n  protected readonly Hash = Hash\n  protected readonly History = History\n  protected readonly KeyRound = KeyRound\n  protected readonly Lock = Lock\n  protected readonly RefreshCw = RefreshCw\n  protected readonly Shield = Shield\n  protected readonly ShieldCheck = ShieldCheck\n  protected readonly Sliders = Sliders\n  protected readonly Trash2 = Trash2\n\n  protected readonly passwordPresets = PASSWORD_PRESETS\n  protected readonly wordPresets = WORD_PRESETS\n  protected readonly pinPresets = PIN_PRESETS\n  protected readonly separators = SEPARATORS\n\n  @Input('class') className?: string\n\n  readonly mode = signal<GeneratorMode>('password')\n  readonly passwordLength = signal(20)\n  readonly includeUpper = signal(true)\n  readonly includeLower = signal(true)\n  readonly includeNumbers = signal(true)\n  readonly includeSymbols = signal(true)\n  readonly avoidAmbiguous = signal(true)\n\n  readonly wordCount = signal(5)\n  readonly separator = signal('-')\n  readonly capitalizeWords = signal(true)\n  readonly includeNumberInPassphrase = signal(true)\n\n  readonly pinLength = signal(6)\n  readonly avoidPinRepeats = signal(true)\n\n  readonly currentSecret = signal('')\n  readonly copied = signal(false)\n  readonly isSpinning = signal(false)\n  readonly copiedHistoryId = signal<string | null>(null)\n  readonly revealed = signal<Record<string, boolean>>({})\n\n  readonly history = signal<HistoryItem[]>([\n    {\n      id: 'hist-1',\n      secret: 'kX9#mQ4$vL2!pW8*jT7^',\n      mode: 'password',\n      entropy: 114,\n      strengthLabel: 'Very Strong',\n      createdAt: 'Just now',\n    },\n    {\n      id: 'hist-2',\n      secret: 'Celestial-Falcon-Lagoon-Timber-83',\n      mode: 'passphrase',\n      entropy: 48,\n      strengthLabel: 'Fair',\n      createdAt: '4m ago',\n    },\n    {\n      id: 'hist-3',\n      secret: '928401',\n      mode: 'pin',\n      entropy: 20,\n      strengthLabel: 'Very Weak',\n      createdAt: '12m ago',\n    },\n  ])\n\n  ngOnInit(): void {\n    this.regenerateSecret(false)\n  }\n\n  readonly currentStrength = computed(() => {\n    let entropy = 0\n    if (this.mode() === 'password') {\n      let poolSize = 0\n      if (this.includeUpper()) poolSize += this.avoidAmbiguous() ? 24 : 26\n      if (this.includeLower()) poolSize += this.avoidAmbiguous() ? 24 : 26\n      if (this.includeNumbers()) poolSize += this.avoidAmbiguous() ? 8 : 10\n      if (this.includeSymbols()) poolSize += this.avoidAmbiguous() ? 26 : 28\n      entropy = poolSize > 1 ? Math.round(this.passwordLength() * Math.log2(poolSize)) : 0\n    } else if (this.mode() === 'passphrase') {\n      entropy = this.wordCount() * Math.log2(WORD_LIST.length)\n      if (this.capitalizeWords()) entropy += this.wordCount() * 1\n      if (this.includeNumberInPassphrase()) entropy += Math.log2(90)\n      entropy = Math.round(entropy)\n    } else {\n      entropy = Math.round(this.pinLength() * Math.log2(10))\n    }\n\n    if (entropy < 30) {\n      return {\n        entropy,\n        score: 1,\n        label: 'Very Weak',\n        crackTime: 'Instant (< 1 ms)',\n        color: 'text-destructive',\n        barClass: 'bg-destructive',\n      }\n    }\n    if (entropy < 50) {\n      return {\n        entropy,\n        score: 2,\n        label: 'Weak',\n        crackTime: 'A few minutes',\n        color: 'text-orange-500',\n        barClass: 'bg-orange-500',\n      }\n    }\n    if (entropy < 70) {\n      return {\n        entropy,\n        score: 3,\n        label: 'Fair',\n        crackTime: 'Several months',\n        color: 'text-warning',\n        barClass: 'bg-warning',\n      }\n    }\n    if (entropy < 95) {\n      return {\n        entropy,\n        score: 4,\n        label: 'Strong',\n        crackTime: 'Hundreds of years',\n        color: 'text-success',\n        barClass: 'bg-success',\n      }\n    }\n    return {\n      entropy,\n      score: 4,\n      label: 'Very Strong',\n      crackTime: '100+ billion years',\n      color: 'text-success',\n      barClass: 'bg-success',\n    }\n  })\n\n  readonly formattedCharacters = computed<FormattedChar[]>(() => {\n    const secret = this.currentSecret()\n    if (!secret) return []\n    return secret.split('').map((char) => ({ char, type: classifyChar(char) }))\n  })\n\n  get rootClass(): string {\n    return cn('mx-auto w-full max-w-4xl space-y-6', this.className)\n  }\n\n  get spinClass(): string {\n    return cn('size-4', this.isSpinning() && 'text-primary animate-spin')\n  }\n\n  get copyClass(): string {\n    return cn('h-9 gap-1.5 px-3.5 active:scale-95', this.copied() && 'bg-success hover:bg-success text-white')\n  }\n\n  charClass(type: FormattedChar['type']): string {\n    if (type === 'number') return 'text-info font-semibold'\n    if (type === 'symbol') return 'text-warning font-semibold'\n    if (type === 'separator') return 'text-muted-foreground px-0.5 font-normal'\n    return 'text-foreground font-medium'\n  }\n\n  segClass(index: number): string {\n    return this.currentStrength().score >= index ? this.currentStrength().barClass : 'bg-muted/80 dark:bg-muted'\n  }\n\n  presetClass(active: boolean): string {\n    return cn(\n      'cursor-pointer rounded-md border px-2.5 py-1 text-xs font-medium transition-colors',\n      active\n        ? 'bg-primary text-primary-foreground border-primary shadow-xs'\n        : 'bg-muted/40 text-muted-foreground border-border hover:bg-muted hover:text-foreground',\n    )\n  }\n\n  separatorClass(active: boolean): string {\n    return cn(\n      'flex cursor-pointer flex-col items-center justify-center rounded-lg border p-2.5 text-xs font-medium transition-colors',\n      active\n        ? 'bg-primary/10 border-primary text-primary font-semibold shadow-xs'\n        : 'border-border bg-background hover:bg-muted/50 text-muted-foreground hover:text-foreground',\n    )\n  }\n\n  sepLabel(label: string): string {\n    return label.split(' ')[0]!\n  }\n\n  historyCopyClass(id: string): string {\n    return cn(\n      'h-8 gap-1.5 px-2.5 text-xs active:scale-95',\n      this.copiedHistoryId() === id && 'bg-success hover:bg-success text-white',\n    )\n  }\n\n  formatItem(text: string): FormattedChar[] {\n    return text.split('').map((char) => ({ char, type: classifyChar(char) }))\n  }\n\n  isRevealed(id: string): boolean {\n    return !!this.revealed()[id]\n  }\n\n  setMode(value: string): void {\n    this.mode.set(value as GeneratorMode)\n    this.regenerateSecret(false)\n  }\n\n  setPasswordLength(value: number): void {\n    this.passwordLength.set(value)\n    this.regenerateSecret(false)\n  }\n\n  setWordCount(value: number): void {\n    this.wordCount.set(value)\n    this.regenerateSecret(false)\n  }\n\n  setPinLength(value: number): void {\n    this.pinLength.set(value)\n    this.regenerateSecret(false)\n  }\n\n  setSeparator(value: string): void {\n    this.separator.set(value)\n    this.regenerateSecret(false)\n  }\n\n  setCapitalizeWords(value: boolean): void {\n    this.capitalizeWords.set(value)\n    this.regenerateSecret(false)\n  }\n\n  setIncludeNumberInPassphrase(value: boolean): void {\n    this.includeNumberInPassphrase.set(value)\n    this.regenerateSecret(false)\n  }\n\n  setAvoidAmbiguous(value: boolean): void {\n    this.avoidAmbiguous.set(value)\n    this.regenerateSecret(false)\n  }\n\n  setAvoidPinRepeats(value: boolean): void {\n    this.avoidPinRepeats.set(value)\n    this.regenerateSecret(false)\n  }\n\n  // At least one character pool stays active in password mode.\n  toggleUpper(val: boolean): void {\n    if (!val && !this.includeLower() && !this.includeNumbers() && !this.includeSymbols()) return\n    this.includeUpper.set(val)\n    this.regenerateSecret(false)\n  }\n\n  toggleLower(val: boolean): void {\n    if (!val && !this.includeUpper() && !this.includeNumbers() && !this.includeSymbols()) return\n    this.includeLower.set(val)\n    this.regenerateSecret(false)\n  }\n\n  toggleNumbers(val: boolean): void {\n    if (!val && !this.includeUpper() && !this.includeLower() && !this.includeSymbols()) return\n    this.includeNumbers.set(val)\n    this.regenerateSecret(false)\n  }\n\n  toggleSymbols(val: boolean): void {\n    if (!val && !this.includeUpper() && !this.includeLower() && !this.includeNumbers()) return\n    this.includeSymbols.set(val)\n    this.regenerateSecret(false)\n  }\n\n  generatePassword(): string {\n    let pool = ''\n    const requiredChars: string[] = []\n\n    const filterPool = (chars: string): string => {\n      if (!this.avoidAmbiguous()) return chars\n      return chars\n        .split('')\n        .filter((c) => !AMBIGUOUS_CHARS.has(c))\n        .join('')\n    }\n\n    const uppers = filterPool(UPPERCASE_CHARS)\n    const lowers = filterPool(LOWERCASE_CHARS)\n    const numbers = filterPool(NUMBER_CHARS)\n    const symbols = filterPool(SYMBOL_CHARS)\n\n    if (this.includeUpper() && uppers.length > 0) {\n      pool += uppers\n      requiredChars.push(uppers[getRandomInt(uppers.length)]!)\n    }\n    if (this.includeLower() && lowers.length > 0) {\n      pool += lowers\n      requiredChars.push(lowers[getRandomInt(lowers.length)]!)\n    }\n    if (this.includeNumbers() && numbers.length > 0) {\n      pool += numbers\n      requiredChars.push(numbers[getRandomInt(numbers.length)]!)\n    }\n    if (this.includeSymbols() && symbols.length > 0) {\n      pool += symbols\n      requiredChars.push(symbols[getRandomInt(symbols.length)]!)\n    }\n\n    if (pool.length === 0) {\n      pool = lowers.length > 0 ? lowers : 'abcdefghjkmnpqrstuvwxyz'\n      requiredChars.push(pool[getRandomInt(pool.length)]!)\n    }\n\n    const result: string[] = [...requiredChars]\n    for (let i = result.length; i < this.passwordLength(); i++) {\n      result.push(pool[getRandomInt(pool.length)]!)\n    }\n\n    for (let i = result.length - 1; i > 0; i--) {\n      const j = getRandomInt(i + 1)\n      const temp = result[i]!\n      result[i] = result[j]!\n      result[j] = temp\n    }\n\n    return result.join('')\n  }\n\n  generatePassphrase(): string {\n    const chosenWords: string[] = []\n    for (let i = 0; i < this.wordCount(); i++) {\n      let word = WORD_LIST[getRandomInt(WORD_LIST.length)]!\n      if (this.capitalizeWords()) word = word.charAt(0).toUpperCase() + word.slice(1)\n      chosenWords.push(word)\n    }\n    if (this.includeNumberInPassphrase()) {\n      chosenWords.push((getRandomInt(90) + 10).toString())\n    }\n    return chosenWords.join(this.separator())\n  }\n\n  generatePin(): string {\n    const digits: string[] = []\n    for (let i = 0; i < this.pinLength(); i++) {\n      let nextDigit = getRandomInt(10).toString()\n      if (this.avoidPinRepeats() && digits.length > 0 && nextDigit === digits[digits.length - 1]) {\n        nextDigit = ((parseInt(nextDigit, 10) + 1 + getRandomInt(8)) % 10).toString()\n      }\n      digits.push(nextDigit)\n    }\n    return digits.join('')\n  }\n\n  regenerateSecret(recordHistory = true): void {\n    let secret = ''\n    if (this.mode() === 'password') secret = this.generatePassword()\n    else if (this.mode() === 'passphrase') secret = this.generatePassphrase()\n    else secret = this.generatePin()\n\n    this.currentSecret.set(secret)\n\n    if (recordHistory && secret) {\n      const strength = this.currentStrength()\n      const item: HistoryItem = {\n        id: `hist-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,\n        secret,\n        mode: this.mode(),\n        entropy: strength.entropy,\n        strengthLabel: strength.label,\n        createdAt: 'Just now',\n      }\n      this.history.update((prev) => [item, ...prev.slice(0, 7)])\n    }\n  }\n\n  handleManualRegenerate(): void {\n    this.isSpinning.set(true)\n    this.regenerateSecret(true)\n    setTimeout(() => this.isSpinning.set(false), 400)\n  }\n\n  async copySecret(text: string, isHistory = false, historyId = ''): Promise<void> {\n    try {\n      if (navigator?.clipboard?.writeText) {\n        await navigator.clipboard.writeText(text)\n      } else {\n        const textarea = document.createElement('textarea')\n        textarea.value = text\n        document.body.appendChild(textarea)\n        textarea.select()\n        document.execCommand('copy')\n        document.body.removeChild(textarea)\n      }\n\n      if (isHistory && historyId) {\n        this.copiedHistoryId.set(historyId)\n        setTimeout(() => {\n          if (this.copiedHistoryId() === historyId) this.copiedHistoryId.set(null)\n        }, 2000)\n      } else {\n        this.copied.set(true)\n        setTimeout(() => this.copied.set(false), 2000)\n      }\n    } catch (err) {\n      console.error('Failed to copy to clipboard', err)\n    }\n  }\n\n  toggleHistoryReveal(id: string): void {\n    this.revealed.update((prev) => ({ ...prev, [id]: !prev[id] }))\n  }\n\n  clearHistory(): void {\n    this.history.set([])\n  }\n}\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/password-generator-widget/password-generator-widget.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/password-generator-widget/index.ts",
      "content": "export {\n  UiPasswordGeneratorWidgetComponent,\n  type HistoryItem,\n  type GeneratorMode,\n} from './password-generator-widget.component'\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/password-generator-widget/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/separator.json",
    "https://uipkge.dev/r/angular/slider.json",
    "https://uipkge.dev/r/angular/switch.json",
    "https://uipkge.dev/r/angular/tabs.json"
  ],
  "description": "1Password and Bitwarden-style customizable password and passphrase generator with entropy strength meter, character type toggles, memorable Diceware passphrases, PIN codes, and volatile history drawer.",
  "categories": [
    "security",
    "auth",
    "app"
  ]
}