{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "exit-intent",
  "title": "Exit Intent",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-react/blocks/exit-intent/ExitIntentNewsletter.tsx",
      "content": "'use client'\n\nimport { useEffect, useState, type FormEvent } from 'react'\nimport { Check, Mail } from 'lucide-react'\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\nexport function ExitIntentNewsletter({ storageKey = 'uipkge:exit-newsletter-seen' }: { storageKey?: string }) {\n  const [open, setOpen] = useState(false)\n  const [email, setEmail] = useState('')\n  const [submitted, setSubmitted] = useState(false)\n\n  useEffect(() => {\n    const 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(storageKey) === '1') return\n        window.sessionStorage.setItem(storageKey, '1')\n      } catch {\n        // Blocked storage: shows once more this page view, never in a loop.\n      }\n      setOpen(true)\n      document.removeEventListener('mouseout', onPointerOut)\n    }\n    document.addEventListener('mouseout', onPointerOut)\n    return () => document.removeEventListener('mouseout', onPointerOut)\n  }, [storageKey])\n\n  function subscribe(event: FormEvent) {\n    event.preventDefault()\n    if (!email.trim()) return\n    setSubmitted(true)\n  }\n\n  return (\n    <div data-slot=\"exit-intent-newsletter\">\n      <Dialog open={open} onOpenChange={setOpen}>\n        <DialogContent className=\"sm:max-w-md\">\n          <DialogHeader>\n            <Badge variant=\"secondary\" className=\"w-fit gap-1.5\">\n              <Mail className=\"size-3\" aria-hidden=\"true\" />\n              Monthly notes\n            </Badge>\n            <DialogTitle className=\"mt-3 text-xl leading-snug text-balance\">\n              Not ready to trial? Take the writing instead\n            </DialogTitle>\n            <DialogDescription className=\"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 onSubmit={subscribe}>\n            <div className=\"flex gap-2\">\n              <Input\n                value={email}\n                onChange={(event) => setEmail(event.target.value)}\n                type=\"email\"\n                required\n                placeholder=\"you@company.com\"\n                autoComplete=\"email\"\n                aria-label=\"Email address\"\n              />\n              <Button type=\"submit\" className=\"shrink-0\">\n                Subscribe\n              </Button>\n            </div>\n            <p className=\"mt-2 min-h-5 text-xs\" aria-live=\"polite\">\n              {submitted ? (\n                <span className=\"text-success inline-flex items-center gap-1.5\">\n                  <Check className=\"size-3\" aria-hidden=\"true\" />\n                  Check your inbox to confirm.\n                </span>\n              ) : (\n                <span className=\"text-muted-foreground\">8,400 subscribers, mostly data and finance engineers.</span>\n              )}\n            </p>\n          </form>\n        </DialogContent>\n      </Dialog>\n    </div>\n  )\n}\n",
      "type": "registry:block",
      "target": "~/components/blocks/exit-intent/ExitIntentNewsletter.tsx"
    },
    {
      "path": "packages/registry-react/blocks/exit-intent/ExitIntentOffer.tsx",
      "content": "'use client'\n\nimport { useCallback, useEffect, useState } from 'react'\nimport { ArrowRight, Clock } from 'lucide-react'\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\nexport function ExitIntentOffer({ storageKey = 'uipkge:exit-intent-seen' }: { storageKey?: string }) {\n  const [open, setOpen] = useState(false)\n\n  const show = useCallback(() => {\n    try {\n      if (window.sessionStorage.getItem(storageKey) === '1') return false\n      window.sessionStorage.setItem(storageKey, '1')\n    } catch {\n      // Blocked storage: it can show once more this page view, never in a loop.\n    }\n    setOpen(true)\n    return true\n  }, [storageKey])\n\n  useEffect(() => {\n    // Only the top edge counts. A pointer leaving left, right, or bottom is\n    // usually someone reaching for a scrollbar or another window.\n    const onPointerOut = (event: MouseEvent) => {\n      if (event.relatedTarget || event.clientY > 4) return\n      if (show()) document.removeEventListener('mouseout', onPointerOut)\n    }\n    document.addEventListener('mouseout', onPointerOut)\n    return () => document.removeEventListener('mouseout', onPointerOut)\n  }, [show])\n\n  return (\n    <div data-slot=\"exit-intent-offer\">\n      <Dialog open={open} onOpenChange={setOpen}>\n        <DialogContent className=\"sm:max-w-md\">\n          <DialogHeader>\n            <Badge variant=\"secondary\" className=\"w-fit gap-1.5\">\n              <Clock className=\"size-3\" aria-hidden=\"true\" />\n              Before you go\n            </Badge>\n            <DialogTitle className=\"mt-3 text-xl leading-snug text-balance\">Take 30 days instead of 14</DialogTitle>\n            <DialogDescription className=\"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 className=\"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 className=\"sm:justify-start\">\n            <Button>\n              Start the 30-day trial\n              <ArrowRight className=\"ml-2 size-4\" aria-hidden=\"true\" />\n            </Button>\n            <Button variant=\"ghost\" onClick={() => setOpen(false)}>\n              No thanks\n            </Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n    </div>\n  )\n}\n",
      "type": "registry:block",
      "target": "~/components/blocks/exit-intent/ExitIntentOffer.tsx"
    },
    {
      "path": "packages/registry-react/blocks/exit-intent/ExitIntentSurvey.tsx",
      "content": "'use client'\n\nimport { useEffect, useState } from 'react'\nimport { Check } from 'lucide-react'\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 options = [\n  'Pricing was unclear',\n  'Missing an integration',\n  'Just browsing',\n  'Security questions',\n  'Something else',\n]\n\nexport function ExitIntentSurvey({ storageKey = 'uipkge:exit-survey-seen' }: { storageKey?: string }) {\n  const [open, setOpen] = useState(false)\n  const [choice, setChoice] = useState('')\n  const [detail, setDetail] = useState('')\n  const [sent, setSent] = useState(false)\n\n  useEffect(() => {\n    const onPointerOut = (event: MouseEvent) => {\n      if (event.relatedTarget || event.clientY > 4) return\n      try {\n        if (window.sessionStorage.getItem(storageKey) === '1') return\n        window.sessionStorage.setItem(storageKey, '1')\n      } catch {\n        // Blocked storage: shows once more this page view, never in a loop.\n      }\n      setOpen(true)\n      document.removeEventListener('mouseout', onPointerOut)\n    }\n    document.addEventListener('mouseout', onPointerOut)\n    return () => document.removeEventListener('mouseout', onPointerOut)\n  }, [storageKey])\n\n  function submit() {\n    if (!choice) return\n    setSent(true)\n    // Close on its own rather than making someone dismiss a thank-you.\n    setTimeout(() => setOpen(false), 1400)\n  }\n\n  return (\n    <div data-slot=\"exit-intent-survey\">\n      <Dialog open={open} onOpenChange={setOpen}>\n        <DialogContent className=\"sm:max-w-md\">\n          {!sent ? (\n            <div>\n              <DialogHeader>\n                <Badge variant=\"secondary\" className=\"w-fit\">\n                  One question\n                </Badge>\n                <DialogTitle className=\"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                value={choice}\n                type=\"single\"\n                variant=\"outline\"\n                size=\"sm\"\n                className=\"mt-5 flex-wrap justify-start\"\n                aria-label=\"Reason for leaving\"\n                onValueChange={(value: string) => setChoice(value ?? '')}\n              >\n                {options.map((option) => (\n                  <ToggleGroupItem key={option} value={option}>\n                    {option}\n                  </ToggleGroupItem>\n                ))}\n              </ToggleGroup>\n\n              <Textarea\n                value={detail}\n                onValueChange={setDetail}\n                className=\"mt-3\"\n                rows={3}\n                placeholder=\"Anything more (optional)\"\n                label=\"Anything more (optional)\"\n              />\n\n              <div className=\"mt-4 flex items-center gap-2\">\n                <Button disabled={!choice} onClick={submit}>\n                  Send\n                </Button>\n                <Button variant=\"ghost\" onClick={() => setOpen(false)}>\n                  Skip\n                </Button>\n              </div>\n            </div>\n          ) : (\n            <div className=\"py-6 text-center\" aria-live=\"polite\">\n              <span className=\"bg-success/10 mx-auto flex size-10 items-center justify-center rounded-full\">\n                <Check className=\"text-success size-5\" aria-hidden=\"true\" />\n              </span>\n              <p className=\"mt-3 text-sm font-medium\">Thanks — that is genuinely useful.</p>\n            </div>\n          )}\n        </DialogContent>\n      </Dialog>\n    </div>\n  )\n}\n",
      "type": "registry:block",
      "target": "~/components/blocks/exit-intent/ExitIntentSurvey.tsx"
    }
  ],
  "dependencies": [
    "lucide-react"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/react/badge.json",
    "https://uipkge.dev/r/react/button.json",
    "https://uipkge.dev/r/react/dialog.json",
    "https://uipkge.dev/r/react/input.json",
    "https://uipkge.dev/r/react/separator.json",
    "https://uipkge.dev/r/react/textarea.json",
    "https://uipkge.dev/r/react/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"
  ]
}