{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "exit-intent",
  "title": "Exit Intent",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-angular/blocks/exit-intent/exit-intent-newsletter.component.ts",
      "content": "// Exit-intent capture for the monthly notes rather than a discount, naming the last issue\n// so the ask is concrete. Fires once per session when the pointer leaves toward the browser\n// chrome. Port of the Vue/React block 1:1.\nimport { ChangeDetectionStrategy, Component, Input, OnDestroy, OnInit, signal } from '@angular/core'\nimport { Check, LucideAngularModule, Mail } 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  UiDialogHeaderComponent,\n  UiDialogTitleComponent,\n} from '@/ui/dialog/dialog.component'\nimport { UiInputComponent } from '@/ui/input/input.component'\nimport { UiSeparatorComponent } from '@/ui/separator/separator.component'\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-exit-intent-newsletter, [ui-exit-intent-newsletter]',\n  standalone: true,\n  // The block renders its own root <div> (and the dialog portals to body): keep the host out of layout.\n  host: { '[attr.class]': '\"contents\"' },\n  imports: [\n    LucideAngularModule,\n    UiBadgeComponent,\n    UiButtonComponent,\n    UiDialogComponent,\n    UiDialogContentComponent,\n    UiDialogDescriptionComponent,\n    UiDialogHeaderComponent,\n    UiDialogTitleComponent,\n    UiInputComponent,\n    UiSeparatorComponent,\n  ],\n  template: `\n    <div data-slot=\"exit-intent-newsletter\">\n      <ui-dialog [open]=\"open()\" (openChange)=\"open.set($event)\">\n        <ui-dialog-content class=\"sm:max-w-md\">\n          <div ui-dialog-header>\n            <span ui-badge variant=\"secondary\" class=\"w-fit gap-1.5\">\n              <lucide-icon [img]=\"Mail\" class=\"size-3\" aria-hidden=\"true\" />\n              Monthly notes\n            </span>\n            <h2 ui-dialog-title class=\"mt-3 text-xl leading-snug text-balance\">\n              Not ready to trial? Take the writing instead\n            </h2>\n            <p ui-dialog-description class=\"leading-relaxed\">\n              Last issue: “Why we version metric definitions instead of dashboards.” One email a month, no product\n              announcements, unsubscribe in one click.\n            </p>\n          </div>\n\n          <ui-separator />\n\n          <form (submit)=\"subscribe($event)\">\n            <div class=\"flex gap-2\">\n              <ui-input\n                [value]=\"email()\"\n                (valueChange)=\"email.set($event)\"\n                type=\"email\"\n                required\n                placeholder=\"you@company.com\"\n                autoComplete=\"email\"\n                aria-label=\"Email address\"\n              />\n              <button ui-button type=\"submit\" class=\"shrink-0\">Subscribe</button>\n            </div>\n            <p class=\"mt-2 min-h-5 text-xs\" aria-live=\"polite\">\n              @if (submitted()) {\n                <span class=\"text-success inline-flex items-center gap-1.5\">\n                  <lucide-icon [img]=\"Check\" class=\"size-3\" aria-hidden=\"true\" />\n                  Check your inbox to confirm.\n                </span>\n              } @else {\n                <span class=\"text-muted-foreground\">8,400 subscribers, mostly data and finance engineers.</span>\n              }\n            </p>\n          </form>\n        </ui-dialog-content>\n      </ui-dialog>\n    </div>\n  `,\n})\nexport class UiExitIntentNewsletterComponent implements OnInit, OnDestroy {\n  protected readonly Check = Check\n  protected readonly Mail = Mail\n\n  @Input() storageKey = 'uipkge:exit-newsletter-seen'\n\n  readonly open = signal(false)\n  readonly email = signal('')\n  readonly submitted = signal(false)\n\n  private readonly onPointerOut = (event: MouseEvent): void => {\n    // Top edge only: leaving sideways is usually the scrollbar or another window.\n    if (event.relatedTarget || event.clientY > 4) return\n    try {\n      if (window.sessionStorage.getItem(this.storageKey) === '1') return\n      window.sessionStorage.setItem(this.storageKey, '1')\n    } catch {\n      // Blocked storage: shows once more this page view, never in a loop.\n    }\n    this.open.set(true)\n    document.removeEventListener('mouseout', this.onPointerOut)\n  }\n\n  ngOnInit(): void {\n    document.addEventListener('mouseout', this.onPointerOut)\n  }\n\n  ngOnDestroy(): void {\n    document.removeEventListener('mouseout', this.onPointerOut)\n  }\n\n  subscribe(event: Event): void {\n    event.preventDefault()\n    if (!this.email().trim()) return\n    this.submitted.set(true)\n  }\n}\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/exit-intent/exit-intent-newsletter.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/exit-intent/exit-intent-offer.component.ts",
      "content": "// Extended-trial offer fired once per session when the pointer leaves toward the browser\n// chrome (top edge only — sideways exits are usually the scrollbar or another window).\n// Port of the Vue/React block 1:1.\nimport { ChangeDetectionStrategy, Component, Input, OnDestroy, OnInit, signal } from '@angular/core'\nimport { ArrowRight, Clock, 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'\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-exit-intent-offer, [ui-exit-intent-offer]',\n  standalone: true,\n  // The block renders its own root <div> (and the dialog portals to body): keep the host out of 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  ],\n  template: `\n    <div data-slot=\"exit-intent-offer\">\n      <ui-dialog [open]=\"open()\" (openChange)=\"open.set($event)\">\n        <ui-dialog-content class=\"sm:max-w-md\">\n          <div ui-dialog-header>\n            <span ui-badge variant=\"secondary\" class=\"w-fit gap-1.5\">\n              <lucide-icon [img]=\"Clock\" class=\"size-3\" aria-hidden=\"true\" />\n              Before you go\n            </span>\n            <h2 ui-dialog-title class=\"mt-3 text-xl leading-snug text-balance\">Take 30 days instead of 14</h2>\n            <p ui-dialog-description class=\"leading-relaxed\">\n              A full close cycle fits in 30 days and 14 does not, which is the actual reason most trials stall. No card,\n              no call, cancel from the dashboard.\n            </p>\n          </div>\n\n          <ui-separator />\n\n          <ul class=\"text-muted-foreground space-y-1.5 text-sm\">\n            <li>· Everything on the Business plan</li>\n            <li>· Connect your real warehouse, read-only</li>\n            <li>· Keep the definitions you author either way</li>\n          </ul>\n\n          <div ui-dialog-footer class=\"sm:justify-start\">\n            <button ui-button>\n              Start the 30-day trial\n              <lucide-icon [img]=\"ArrowRight\" class=\"ml-2 size-4\" aria-hidden=\"true\" />\n            </button>\n            <button ui-button variant=\"ghost\" (click)=\"open.set(false)\">No thanks</button>\n          </div>\n        </ui-dialog-content>\n      </ui-dialog>\n    </div>\n  `,\n})\nexport class UiExitIntentOfferComponent implements OnInit, OnDestroy {\n  protected readonly ArrowRight = ArrowRight\n  protected readonly Clock = Clock\n\n  @Input() storageKey = 'uipkge:exit-intent-seen'\n\n  readonly open = signal(false)\n\n  private readonly onPointerOut = (event: MouseEvent): void => {\n    if (event.relatedTarget || event.clientY > 4) return\n    if (this.show()) document.removeEventListener('mouseout', this.onPointerOut)\n  }\n\n  ngOnInit(): void {\n    document.addEventListener('mouseout', this.onPointerOut)\n  }\n\n  ngOnDestroy(): void {\n    document.removeEventListener('mouseout', this.onPointerOut)\n  }\n\n  show(): boolean {\n    try {\n      if (window.sessionStorage.getItem(this.storageKey) === '1') return false\n      window.sessionStorage.setItem(this.storageKey, '1')\n    } catch {\n      // Blocked storage: it can show once more this page view, never in a loop.\n    }\n    this.open.set(true)\n    return true\n  }\n}\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/exit-intent/exit-intent-offer.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/exit-intent/exit-intent-survey.component.ts",
      "content": "// One-question exit survey asking what the visitor did not find, with preset answers, an\n// optional free-text field, and a thank-you state that closes itself. Fires once per session\n// when the pointer leaves toward the browser chrome. Port of the Vue/React block 1:1.\nimport { ChangeDetectionStrategy, Component, DestroyRef, Input, OnDestroy, OnInit, inject, signal } from '@angular/core'\nimport { Check, 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  UiDialogHeaderComponent,\n  UiDialogTitleComponent,\n} from '@/ui/dialog/dialog.component'\nimport { UiTextareaComponent } from '@/ui/textarea/textarea.component'\nimport { UiToggleGroupComponent, UiToggleGroupItemComponent } from '@/ui/toggle-group/toggle-group.component'\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-exit-intent-survey, [ui-exit-intent-survey]',\n  standalone: true,\n  // The block renders its own root <div> (and the dialog portals to body): keep the host out of layout.\n  host: { '[attr.class]': '\"contents\"' },\n  imports: [\n    LucideAngularModule,\n    UiBadgeComponent,\n    UiButtonComponent,\n    UiDialogComponent,\n    UiDialogContentComponent,\n    UiDialogDescriptionComponent,\n    UiDialogHeaderComponent,\n    UiDialogTitleComponent,\n    UiTextareaComponent,\n    UiToggleGroupComponent,\n    UiToggleGroupItemComponent,\n  ],\n  template: `\n    <div data-slot=\"exit-intent-survey\">\n      <ui-dialog [open]=\"open()\" (openChange)=\"open.set($event)\">\n        <ui-dialog-content class=\"sm:max-w-md\">\n          @if (!sent()) {\n            <div>\n              <div ui-dialog-header>\n                <span ui-badge variant=\"secondary\" class=\"w-fit\">One question</span>\n                <h2 ui-dialog-title class=\"mt-3 text-xl leading-snug text-balance\">What were you looking for?</h2>\n                <p ui-dialog-description>\n                  It goes to the people who build the page. No follow-up email unless you ask for one.\n                </p>\n              </div>\n\n              <ui-toggle-group\n                type=\"single\"\n                [value]=\"choice()\"\n                variant=\"outline\"\n                size=\"sm\"\n                class=\"mt-5 flex-wrap justify-start\"\n                aria-label=\"Reason for leaving\"\n                (valueChange)=\"onChoiceChange($event)\"\n              >\n                @for (option of options; track option) {\n                  <button ui-toggle-group-item [value]=\"option\">{{ option }}</button>\n                }\n              </ui-toggle-group>\n\n              <ui-textarea\n                [value]=\"detail()\"\n                (valueChange)=\"detail.set($event)\"\n                class=\"mt-3\"\n                [rows]=\"3\"\n                placeholder=\"Anything more (optional)\"\n                label=\"Anything more (optional)\"\n              />\n\n              <div class=\"mt-4 flex items-center gap-2\">\n                <button ui-button [disabled]=\"!choice()\" (click)=\"submit()\">Send</button>\n                <button ui-button variant=\"ghost\" (click)=\"open.set(false)\">Skip</button>\n              </div>\n            </div>\n          } @else {\n            <div class=\"py-6 text-center\" aria-live=\"polite\">\n              <span class=\"bg-success/10 mx-auto flex size-10 items-center justify-center rounded-full\">\n                <lucide-icon [img]=\"Check\" class=\"text-success size-5\" aria-hidden=\"true\" />\n              </span>\n              <p class=\"mt-3 text-sm font-medium\">Thanks — that is genuinely useful.</p>\n            </div>\n          }\n        </ui-dialog-content>\n      </ui-dialog>\n    </div>\n  `,\n})\nexport class UiExitIntentSurveyComponent implements OnInit, OnDestroy {\n  protected readonly Check = Check\n\n  @Input() storageKey = 'uipkge:exit-survey-seen'\n\n  readonly open = signal(false)\n  readonly choice = signal('')\n  readonly detail = signal('')\n  readonly sent = signal(false)\n\n  protected readonly options = [\n    'Pricing was unclear',\n    'Missing an integration',\n    'Just browsing',\n    'Security questions',\n    'Something else',\n  ]\n\n  private sendTimer: ReturnType<typeof setTimeout> | undefined\n\n  private readonly onPointerOut = (event: MouseEvent): void => {\n    if (event.relatedTarget || event.clientY > 4) return\n    try {\n      if (window.sessionStorage.getItem(this.storageKey) === '1') return\n      window.sessionStorage.setItem(this.storageKey, '1')\n    } catch {\n      // Blocked storage: shows once more this page view, never in a loop.\n    }\n    this.open.set(true)\n    document.removeEventListener('mouseout', this.onPointerOut)\n  }\n\n  constructor() {\n    inject(DestroyRef).onDestroy(() => clearTimeout(this.sendTimer))\n  }\n\n  ngOnInit(): void {\n    document.addEventListener('mouseout', this.onPointerOut)\n  }\n\n  ngOnDestroy(): void {\n    document.removeEventListener('mouseout', this.onPointerOut)\n  }\n\n  onChoiceChange(value: string): void {\n    this.choice.set(value ?? '')\n  }\n\n  submit(): void {\n    if (!this.choice()) return\n    this.sent.set(true)\n    // Close on its own rather than making someone dismiss a thank-you.\n    clearTimeout(this.sendTimer)\n    this.sendTimer = setTimeout(() => this.open.set(false), 1400)\n  }\n}\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/exit-intent/exit-intent-survey.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/exit-intent/index.ts",
      "content": "export { UiExitIntentNewsletterComponent } from './exit-intent-newsletter.component'\nexport { UiExitIntentOfferComponent } from './exit-intent-offer.component'\nexport { UiExitIntentSurveyComponent } from './exit-intent-survey.component'\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/exit-intent/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/input.json",
    "https://uipkge.dev/r/angular/separator.json",
    "https://uipkge.dev/r/angular/textarea.json",
    "https://uipkge.dev/r/angular/toggle-group.json"
  ],
  "description": "Exit-intent dialog trio that fires once per session when the pointer leaves toward the browser chrome: an extended-trial offer, a monthly-notes newsletter capture, and a one-question exit survey.",
  "categories": [
    "marketing"
  ]
}