{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "exit-intent",
  "title": "Exit Intent",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-vue/blocks/exit-intent/ExitIntentNewsletter.vue",
      "content": "<script setup lang=\"ts\">\nimport { onBeforeUnmount, onMounted, ref } from 'vue'\nimport { Check, Mail } from 'lucide-vue-next'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'\nimport { Input } from '@/components/ui/input'\nimport { Separator } from '@/components/ui/separator'\n\nconst props = withDefaults(defineProps<{ storageKey?: string }>(), {\n  storageKey: 'uipkge:exit-newsletter-seen',\n})\n\nconst open = ref(false)\nconst email = ref('')\nconst submitted = ref(false)\n\nfunction onPointerOut(event: MouseEvent) {\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(props.storageKey) === '1') return\n    window.sessionStorage.setItem(props.storageKey, '1')\n  } catch {\n    // Blocked storage: shows once more this page view, never in a loop.\n  }\n  open.value = true\n  document.removeEventListener('mouseout', onPointerOut)\n}\n\nfunction subscribe() {\n  if (!email.value.trim()) return\n  submitted.value = true\n}\n\nonMounted(() => document.addEventListener('mouseout', onPointerOut))\nonBeforeUnmount(() => document.removeEventListener('mouseout', onPointerOut))\n</script>\n\n<template>\n  <div data-slot=\"exit-intent-newsletter\">\n    <Dialog v-model:open=\"open\">\n      <DialogContent class=\"sm:max-w-md\">\n        <DialogHeader>\n          <Badge variant=\"secondary\" class=\"w-fit gap-1.5\">\n            <Mail class=\"size-3\" aria-hidden=\"true\" />\n            Monthly notes\n          </Badge>\n          <DialogTitle class=\"mt-3 text-xl leading-snug text-balance\">\n            Not ready to trial? Take the writing instead\n          </DialogTitle>\n          <DialogDescription 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          </DialogDescription>\n        </DialogHeader>\n\n        <Separator />\n\n        <form @submit.prevent=\"subscribe\">\n          <div class=\"flex gap-2\">\n            <Input\n              v-model=\"email\"\n              type=\"email\"\n              required\n              placeholder=\"you@company.com\"\n              autocomplete=\"email\"\n              aria-label=\"Email address\"\n            />\n            <Button type=\"submit\" class=\"shrink-0\">Subscribe</Button>\n          </div>\n          <p class=\"mt-2 min-h-5 text-xs\" aria-live=\"polite\">\n            <span v-if=\"submitted\" class=\"text-success inline-flex items-center gap-1.5\">\n              <Check class=\"size-3\" aria-hidden=\"true\" />\n              Check your inbox to confirm.\n            </span>\n            <span v-else class=\"text-muted-foreground\">8,400 subscribers, mostly data and finance engineers.</span>\n          </p>\n        </form>\n      </DialogContent>\n    </Dialog>\n  </div>\n</template>\n",
      "type": "registry:block",
      "target": "~/app/components/blocks/exit-intent/ExitIntentNewsletter.vue"
    },
    {
      "path": "packages/registry-vue/blocks/exit-intent/ExitIntentOffer.vue",
      "content": "<script setup lang=\"ts\">\nimport { onBeforeUnmount, onMounted, ref } from 'vue'\nimport { ArrowRight, Clock } from 'lucide-vue-next'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from '@/components/ui/dialog'\nimport { Separator } from '@/components/ui/separator'\n\nconst props = withDefaults(defineProps<{ storageKey?: string }>(), {\n  storageKey: 'uipkge:exit-intent-seen',\n})\n\nconst open = ref(false)\n\n// Only the top edge counts. A pointer leaving left, right, or bottom is usually\n// someone reaching for a scrollbar or another window, not leaving the page.\nfunction onPointerOut(event: MouseEvent) {\n  if (event.relatedTarget || event.clientY > 4) return\n  show()\n}\n\nfunction show() {\n  try {\n    if (window.sessionStorage.getItem(props.storageKey) === '1') return\n    window.sessionStorage.setItem(props.storageKey, '1')\n  } catch {\n    // Blocked storage: it can show once more this page view, never in a loop.\n  }\n  open.value = true\n  document.removeEventListener('mouseout', onPointerOut)\n}\n\nonMounted(() => document.addEventListener('mouseout', onPointerOut))\nonBeforeUnmount(() => document.removeEventListener('mouseout', onPointerOut))\n</script>\n\n<template>\n  <div data-slot=\"exit-intent-offer\">\n    <Dialog v-model:open=\"open\">\n      <DialogContent class=\"sm:max-w-md\">\n        <DialogHeader>\n          <Badge variant=\"secondary\" class=\"w-fit gap-1.5\">\n            <Clock class=\"size-3\" aria-hidden=\"true\" />\n            Before you go\n          </Badge>\n          <DialogTitle class=\"mt-3 text-xl leading-snug text-balance\"> Take 30 days instead of 14 </DialogTitle>\n          <DialogDescription 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          </DialogDescription>\n        </DialogHeader>\n\n        <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        <DialogFooter class=\"sm:justify-start\">\n          <Button>\n            Start the 30-day trial\n            <ArrowRight class=\"ml-2 size-4\" aria-hidden=\"true\" />\n          </Button>\n          <Button variant=\"ghost\" @click=\"open = false\">No thanks</Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  </div>\n</template>\n",
      "type": "registry:block",
      "target": "~/app/components/blocks/exit-intent/ExitIntentOffer.vue"
    },
    {
      "path": "packages/registry-vue/blocks/exit-intent/ExitIntentSurvey.vue",
      "content": "<script setup lang=\"ts\">\nimport { onBeforeUnmount, onMounted, ref } from 'vue'\nimport { Check } from 'lucide-vue-next'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'\nimport { Textarea } from '@/components/ui/textarea'\nimport { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'\n\nconst props = withDefaults(defineProps<{ storageKey?: string }>(), {\n  storageKey: 'uipkge:exit-survey-seen',\n})\n\nconst options = [\n  'Pricing was unclear',\n  'Missing an integration',\n  'Just browsing',\n  'Security questions',\n  'Something else',\n]\n\nconst open = ref(false)\nconst choice = ref('')\nconst detail = ref('')\nconst sent = ref(false)\n\nfunction onPointerOut(event: MouseEvent) {\n  if (event.relatedTarget || event.clientY > 4) return\n  try {\n    if (window.sessionStorage.getItem(props.storageKey) === '1') return\n    window.sessionStorage.setItem(props.storageKey, '1')\n  } catch {\n    // Blocked storage: shows once more this page view, never in a loop.\n  }\n  open.value = true\n  document.removeEventListener('mouseout', onPointerOut)\n}\n\nfunction submit() {\n  if (!choice.value) return\n  sent.value = true\n  // Close on its own rather than making someone dismiss a thank-you.\n  setTimeout(() => (open.value = false), 1400)\n}\n\nonMounted(() => document.addEventListener('mouseout', onPointerOut))\nonBeforeUnmount(() => document.removeEventListener('mouseout', onPointerOut))\n</script>\n\n<template>\n  <div data-slot=\"exit-intent-survey\">\n    <Dialog v-model:open=\"open\">\n      <DialogContent class=\"sm:max-w-md\">\n        <div v-if=\"!sent\">\n          <DialogHeader>\n            <Badge variant=\"secondary\" class=\"w-fit\">One question</Badge>\n            <DialogTitle class=\"mt-3 text-xl leading-snug text-balance\">What were you looking for?</DialogTitle>\n            <DialogDescription>\n              It goes to the people who build the page. No follow-up email unless you ask for one.\n            </DialogDescription>\n          </DialogHeader>\n\n          <ToggleGroup\n            :model-value=\"choice\"\n            type=\"single\"\n            variant=\"outline\"\n            size=\"sm\"\n            class=\"mt-5 flex-wrap justify-start\"\n            aria-label=\"Reason for leaving\"\n            @update:model-value=\"(value) => (choice = typeof value === 'string' ? value : '')\"\n          >\n            <ToggleGroupItem v-for=\"option in options\" :key=\"option\" :value=\"option\">{{ option }}</ToggleGroupItem>\n          </ToggleGroup>\n\n          <Textarea\n            v-model=\"detail\"\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 :disabled=\"!choice\" @click=\"submit\">Send</Button>\n            <Button variant=\"ghost\" @click=\"open = false\">Skip</Button>\n          </div>\n        </div>\n\n        <div v-else 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            <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      </DialogContent>\n    </Dialog>\n  </div>\n</template>\n",
      "type": "registry:block",
      "target": "~/app/components/blocks/exit-intent/ExitIntentSurvey.vue"
    }
  ],
  "dependencies": [
    "lucide-vue-next"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/vue/badge.json",
    "https://uipkge.dev/r/vue/button.json",
    "https://uipkge.dev/r/vue/dialog.json",
    "https://uipkge.dev/r/vue/input.json",
    "https://uipkge.dev/r/vue/separator.json",
    "https://uipkge.dev/r/vue/textarea.json",
    "https://uipkge.dev/r/vue/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"
  ]
}