{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "cookie-consent",
  "title": "Cookie Consent",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-angular/blocks/cookie-consent/cookie-consent-banner.component.ts",
      "content": "// GDPR-style cookie consent banner with two variants: a full-width bottom bar and a compact\n// card. Customize expands inline preferences with Necessary (always on), Analytics, and\n// Marketing switches. Accept, Reject, and Save dismiss with a 150ms fade.\n// Port of the Vue/React block 1:1.\nimport { ChangeDetectionStrategy, Component, Input, signal } from '@angular/core'\nimport { Cookie, LucideAngularModule } from 'lucide-angular'\nimport { cn } from '@/lib/utils'\nimport { UiButtonComponent } from '@/ui/button/button.component'\nimport { UiSeparatorComponent } from '@/ui/separator/separator.component'\nimport { UiSwitchComponent } from '@/ui/switch/switch.component'\n\nexport type CookieConsentBannerVariant = 'bottom-bar' | 'card'\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-cookie-consent-banner, [ui-cookie-consent-banner]',\n  standalone: true,\n  // The block renders its own root <div>: keep this wrapper out of the layout.\n  host: { '[attr.class]': '\"contents\"' },\n  imports: [LucideAngularModule, UiButtonComponent, UiSeparatorComponent, UiSwitchComponent],\n  template: `\n    @if (visible()) {\n      <div data-slot=\"cookie-consent-banner\" role=\"dialog\" aria-label=\"Cookie consent\" [class]=\"rootClass\">\n        @if (variant === 'bottom-bar') {\n          <div class=\"bg-card border-border w-full border-t px-4 py-4 sm:px-6\">\n            <div class=\"mx-auto flex max-w-5xl flex-col gap-4\">\n              <div class=\"flex flex-col gap-4 md:flex-row md:items-center md:justify-between\">\n                <div class=\"flex items-start gap-3\">\n                  <span\n                    class=\"bg-primary/10 text-primary mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-full\"\n                  >\n                    <lucide-icon [img]=\"Cookie\" class=\"size-4\" aria-hidden=\"true\" />\n                  </span>\n                  <p class=\"text-muted-foreground max-w-xl text-sm leading-relaxed\">\n                    We use cookies to improve your experience and analyze traffic. Read our\n                    <a\n                      href=\"#\"\n                      class=\"text-foreground focus-visible:ring-ring rounded-sm font-medium underline underline-offset-4 outline-none focus-visible:ring-2\"\n                      >Cookie Policy</a\n                    >.\n                  </p>\n                </div>\n                <div class=\"flex shrink-0 flex-wrap items-center gap-2\">\n                  <button ui-button variant=\"ghost\" size=\"sm\" (click)=\"showPreferences.set(!showPreferences())\">\n                    {{ showPreferences() ? 'Hide preferences' : 'Customize' }}\n                  </button>\n                  <button ui-button variant=\"outline\" size=\"sm\" (click)=\"dismiss()\">Reject</button>\n                  <button ui-button size=\"sm\" (click)=\"dismiss()\">Accept all</button>\n                </div>\n              </div>\n\n              @if (showPreferences()) {\n                <div class=\"flex flex-col gap-3\">\n                  <ui-separator />\n                  <div class=\"flex items-center justify-between gap-4 py-1\">\n                    <div>\n                      <p class=\"text-sm font-medium\">Necessary</p>\n                      <p class=\"text-muted-foreground text-xs\">Required for the site to function.</p>\n                    </div>\n                    <ui-switch [checked]=\"true\" disabled aria-label=\"Necessary cookies\" />\n                  </div>\n                  <div class=\"flex items-center justify-between gap-4 py-1\">\n                    <div>\n                      <p class=\"text-sm font-medium\">Analytics</p>\n                      <p class=\"text-muted-foreground text-xs\">Helps us understand usage patterns.</p>\n                    </div>\n                    <ui-switch\n                      [checked]=\"analytics()\"\n                      (checkedChange)=\"analytics.set($event)\"\n                      aria-label=\"Analytics cookies\"\n                    />\n                  </div>\n                  <div class=\"flex items-center justify-between gap-4 py-1\">\n                    <div>\n                      <p class=\"text-sm font-medium\">Marketing</p>\n                      <p class=\"text-muted-foreground text-xs\">Used to personalize campaigns.</p>\n                    </div>\n                    <ui-switch\n                      [checked]=\"marketing()\"\n                      (checkedChange)=\"marketing.set($event)\"\n                      aria-label=\"Marketing cookies\"\n                    />\n                  </div>\n                  <div class=\"mt-1 flex justify-end\">\n                    <button ui-button size=\"sm\" (click)=\"dismiss()\">Save preferences</button>\n                  </div>\n                </div>\n              }\n            </div>\n          </div>\n        } @else {\n          <div class=\"bg-card border-border mx-auto max-w-md rounded-xl border p-6 shadow-xs\">\n            <div class=\"flex flex-col gap-4\">\n              <div class=\"flex items-start gap-3\">\n                <span class=\"bg-primary/10 text-primary flex size-9 shrink-0 items-center justify-center rounded-full\">\n                  <lucide-icon [img]=\"Cookie\" class=\"size-4\" aria-hidden=\"true\" />\n                </span>\n                <div>\n                  <h3 class=\"text-sm font-semibold\">We value your privacy</h3>\n                  <p class=\"text-muted-foreground mt-1 text-sm leading-relaxed\">\n                    We use cookies to improve your experience and analyze traffic. Read our\n                    <a\n                      href=\"#\"\n                      class=\"text-foreground focus-visible:ring-ring rounded-sm font-medium underline underline-offset-4 outline-none focus-visible:ring-2\"\n                      >Cookie Policy</a\n                    >.\n                  </p>\n                </div>\n              </div>\n\n              @if (showPreferences()) {\n                <ui-separator />\n                <div class=\"flex items-center justify-between gap-4 py-1\">\n                  <div>\n                    <p class=\"text-sm font-medium\">Necessary</p>\n                    <p class=\"text-muted-foreground text-xs\">Required for the site to function.</p>\n                  </div>\n                  <ui-switch [checked]=\"true\" disabled aria-label=\"Necessary cookies\" />\n                </div>\n                <div class=\"flex items-center justify-between gap-4 py-1\">\n                  <div>\n                    <p class=\"text-sm font-medium\">Analytics</p>\n                    <p class=\"text-muted-foreground text-xs\">Helps us understand usage patterns.</p>\n                  </div>\n                  <ui-switch\n                    [checked]=\"analytics()\"\n                    (checkedChange)=\"analytics.set($event)\"\n                    aria-label=\"Analytics cookies\"\n                  />\n                </div>\n                <div class=\"flex items-center justify-between gap-4 py-1\">\n                  <div>\n                    <p class=\"text-sm font-medium\">Marketing</p>\n                    <p class=\"text-muted-foreground text-xs\">Used to personalize campaigns.</p>\n                  </div>\n                  <ui-switch\n                    [checked]=\"marketing()\"\n                    (checkedChange)=\"marketing.set($event)\"\n                    aria-label=\"Marketing cookies\"\n                  />\n                </div>\n              }\n\n              <div class=\"flex flex-wrap items-center gap-2\">\n                <button ui-button variant=\"ghost\" size=\"sm\" (click)=\"showPreferences.set(!showPreferences())\">\n                  {{ showPreferences() ? 'Hide preferences' : 'Customize' }}\n                </button>\n                <div class=\"ml-auto flex items-center gap-2\">\n                  <button ui-button variant=\"outline\" size=\"sm\" (click)=\"dismiss()\">Reject</button>\n                  <button ui-button size=\"sm\" (click)=\"dismiss()\">\n                    {{ showPreferences() ? 'Save preferences' : 'Accept all' }}\n                  </button>\n                </div>\n              </div>\n            </div>\n          </div>\n        }\n      </div>\n    }\n  `,\n})\nexport class UiCookieConsentBannerComponent {\n  protected readonly Cookie = Cookie\n\n  @Input() variant: CookieConsentBannerVariant = 'bottom-bar'\n  @Input('class') className?: string\n\n  readonly visible = signal(true)\n  readonly showPreferences = signal(false)\n  readonly analytics = signal(true)\n  readonly marketing = signal(false)\n\n  get rootClass(): string {\n    return cn('animate-in fade-in duration-150', this.className)\n  }\n\n  dismiss(): void {\n    this.visible.set(false)\n  }\n}\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/cookie-consent/cookie-consent-banner.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/cookie-consent/cookie-consent-bar.component.ts",
      "content": "// Bottom consent bar offering accept, reject, and manage as equally weighted actions, with a\n// one-line statement of what is actually stored. Reject carries the same visual weight as\n// accept — a ghost \"reject\" beside a solid \"accept\" is a dark pattern with extra steps.\n// Port of the Vue/React block 1:1.\nimport { ChangeDetectionStrategy, Component, Input, OnInit, signal } from '@angular/core'\nimport { Cookie, LucideAngularModule } from 'lucide-angular'\nimport { UiBadgeComponent } from '@/ui/badge/badge.component'\nimport { UiButtonComponent } from '@/ui/button/button.component'\nimport { UiSeparatorComponent } from '@/ui/separator/separator.component'\n\nexport type CookieConsentChoice = 'accepted' | 'rejected'\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-cookie-consent-bar, [ui-cookie-consent-bar]',\n  standalone: true,\n  // The block renders its own fixed root <div>: keep this wrapper out of the layout.\n  host: { '[attr.class]': '\"contents\"' },\n  imports: [LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiSeparatorComponent],\n  template: `\n    @if (visible()) {\n      <div\n        data-slot=\"cookie-consent-bar\"\n        class=\"border-border bg-card/95 fixed inset-x-0 bottom-0 z-50 border-t backdrop-blur\"\n        role=\"region\"\n        aria-label=\"Cookie consent\"\n      >\n        <div class=\"mx-auto flex max-w-6xl flex-col gap-4 px-6 py-4 lg:flex-row lg:items-center lg:gap-8\">\n          <div class=\"flex min-w-0 items-start gap-3\">\n            <lucide-icon [img]=\"Cookie\" class=\"text-muted-foreground mt-0.5 size-4 shrink-0\" aria-hidden=\"true\" />\n            <div class=\"min-w-0\">\n              <div class=\"flex flex-wrap items-center gap-2\">\n                <p class=\"text-sm font-medium\">We store two things</p>\n                <span ui-badge variant=\"outline\">No ad tracking</span>\n              </div>\n              <p class=\"text-muted-foreground mt-1 text-sm leading-relaxed\">\n                A session cookie so you stay signed in, and an analytics cookie recording which pages get read.\n                Rejecting keeps the first and drops the second.\n              </p>\n            </div>\n          </div>\n\n          <ui-separator orientation=\"vertical\" class=\"hidden h-10 lg:block\" />\n\n          <!-- Reject carries the same visual weight as accept. A ghost \"reject\"\n               beside a solid \"accept\" is a dark pattern with extra steps. -->\n          <div class=\"flex shrink-0 flex-wrap items-center gap-2\">\n            <button ui-button variant=\"outline\" (click)=\"decide('rejected')\">Reject analytics</button>\n            <button ui-button (click)=\"decide('accepted')\">Accept</button>\n            <button ui-button variant=\"ghost\" size=\"sm\">Manage</button>\n          </div>\n        </div>\n      </div>\n    }\n  `,\n})\nexport class UiCookieConsentBarComponent implements OnInit {\n  protected readonly Cookie = Cookie\n\n  @Input() storageKey = 'uipkge:consent'\n\n  readonly visible = signal(false)\n\n  ngOnInit(): void {\n    try {\n      this.visible.set(!window.localStorage.getItem(this.storageKey))\n    } catch {\n      this.visible.set(true)\n    }\n  }\n\n  decide(choice: CookieConsentChoice): void {\n    this.visible.set(false)\n    try {\n      window.localStorage.setItem(this.storageKey, choice)\n    } catch {\n      // Blocked storage: the choice holds for this page view only.\n    }\n  }\n}\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/cookie-consent/cookie-consent-bar.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/cookie-consent/cookie-consent-preferences.component.ts",
      "content": "// Granular cookie consent dialog: each category states its purpose and retention, with a\n// switch. Strictly-necessary is listed and shown locked rather than hidden — omitting it\n// implies there is nothing there, which is not true. Port of the Vue/React block 1:1.\nimport { ChangeDetectionStrategy, Component, signal } from '@angular/core'\nimport { Lock, LucideAngularModule } from 'lucide-angular'\nimport { UiBadgeComponent } from '@/ui/badge/badge.component'\nimport { UiButtonComponent } from '@/ui/button/button.component'\nimport {\n  UiDialogComponent,\n  UiDialogContentComponent,\n  UiDialogDescriptionComponent,\n  UiDialogFooterComponent,\n  UiDialogHeaderComponent,\n  UiDialogTitleComponent,\n} from '@/ui/dialog/dialog.component'\nimport { UiSeparatorComponent } from '@/ui/separator/separator.component'\nimport { UiSwitchComponent } from '@/ui/switch/switch.component'\n\nexport interface CookieCategory {\n  id: string\n  label: string\n  purpose: string\n  retention: string\n  locked: boolean\n}\n\nconst categories: CookieCategory[] = [\n  {\n    id: 'necessary',\n    label: 'Strictly necessary',\n    purpose: 'Session and sign-in state. Without these the app cannot keep you logged in.',\n    retention: 'Session',\n    locked: true,\n  },\n  {\n    id: 'analytics',\n    label: 'Product analytics',\n    purpose: 'Which pages are read and where people give up. Aggregated, never sold.',\n    retention: '13 months',\n    locked: false,\n  },\n  {\n    id: 'support',\n    label: 'Support chat',\n    purpose: 'Keeps a conversation open across page loads so you do not repeat yourself.',\n    retention: '30 days',\n    locked: false,\n  },\n]\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-cookie-consent-preferences, [ui-cookie-consent-preferences]',\n  standalone: true,\n  // The block renders its own root <div>: keep this wrapper out of the layout.\n  host: { '[attr.class]': '\"contents\"' },\n  imports: [\n    LucideAngularModule,\n    UiBadgeComponent,\n    UiButtonComponent,\n    UiDialogComponent,\n    UiDialogContentComponent,\n    UiDialogDescriptionComponent,\n    UiDialogFooterComponent,\n    UiDialogHeaderComponent,\n    UiDialogTitleComponent,\n    UiSeparatorComponent,\n    UiSwitchComponent,\n  ],\n  template: `\n    <div data-slot=\"cookie-consent-preferences\">\n      <div ui-dialog [open]=\"open()\" (openChange)=\"open.set($event)\">\n        <div ui-dialog-content class=\"sm:max-w-lg\">\n          <div ui-dialog-header>\n            <span ui-badge variant=\"secondary\" class=\"w-fit\">Privacy</span>\n            <h2 ui-dialog-title class=\"mt-3 text-xl\">Choose what we store</h2>\n            <p ui-dialog-description>\n              Each category says what it is for and how long it is kept. Nothing here is advertising.\n            </p>\n          </div>\n\n          <ui-separator />\n\n          <ul class=\"space-y-4\">\n            @for (category of categories; track category.id) {\n              <li class=\"flex items-start gap-4\">\n                <div class=\"min-w-0 flex-1\">\n                  <div class=\"flex flex-wrap items-center gap-2\">\n                    <p class=\"text-sm font-medium\">{{ category.label }}</p>\n                    @if (category.locked) {\n                      <span ui-badge variant=\"outline\" class=\"gap-1\">\n                        <lucide-icon [img]=\"Lock\" class=\"size-2.5\" aria-hidden=\"true\" />\n                        Required\n                      </span>\n                    }\n                  </div>\n                  <p class=\"text-muted-foreground mt-1 text-xs leading-relaxed\">{{ category.purpose }}</p>\n                  <p class=\"text-muted-foreground/80 mt-1 font-mono text-xs\">Kept {{ category.retention }}</p>\n                </div>\n                <ui-switch\n                  [checked]=\"choices()[category.id] ?? false\"\n                  (checkedChange)=\"setChoice(category.id, $event)\"\n                  [disabled]=\"category.locked\"\n                  [attr.aria-label]=\"'Allow ' + category.label\"\n                  class=\"mt-1 shrink-0\"\n                />\n              </li>\n            }\n          </ul>\n\n          <div ui-dialog-footer class=\"sm:justify-start\">\n            <button ui-button (click)=\"open.set(false)\">Save choices</button>\n            <button ui-button variant=\"outline\" (click)=\"open.set(false)\">Reject optional</button>\n          </div>\n        </div>\n      </div>\n    </div>\n  `,\n})\nexport class UiCookieConsentPreferencesComponent {\n  protected readonly Lock = Lock\n  protected readonly categories = categories\n\n  readonly open = signal(true)\n  readonly choices = signal<Record<string, boolean>>({ necessary: true, analytics: false, support: false })\n\n  setChoice(id: string, value: boolean): void {\n    this.choices.update((prev) => ({ ...prev, [id]: value }))\n  }\n}\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/cookie-consent/cookie-consent-preferences.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/cookie-consent/index.ts",
      "content": "export { UiCookieConsentBannerComponent, type CookieConsentBannerVariant } from './cookie-consent-banner.component'\nexport { UiCookieConsentBarComponent, type CookieConsentChoice } from './cookie-consent-bar.component'\nexport { UiCookieConsentPreferencesComponent, type CookieCategory } from './cookie-consent-preferences.component'\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/cookie-consent/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/dialog.json",
    "https://uipkge.dev/r/angular/separator.json",
    "https://uipkge.dev/r/angular/switch.json"
  ],
  "description": "Consent trio covering the whole banner-to-dialog flow: a full-width banner with inline preferences, a bottom bar with equally weighted accept, reject, and manage actions, and a granular preferences dialog with per-category purpose, retention, and switches.",
  "categories": [
    "legal",
    "marketing",
    "feedback"
  ]
}