{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sticky-cta",
  "title": "Sticky CTA",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-react/blocks/sticky-cta/StickyCtaBar.tsx",
      "content": "'use client'\n\nimport { useEffect, useRef, useState } from 'react'\nimport { Clock, X } from 'lucide-react'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport { Separator } from '@/components/ui/separator'\n\ninterface StickyCtaBarProps {\n  /** Scroll distance in pixels before the bar reveals. */\n  threshold?: number\n  /** sessionStorage key holding the dismissal, so it stays closed for the session. */\n  storageKey?: string\n}\n\nexport function StickyCtaBar({ threshold = 480, storageKey = 'uipkge:sticky-cta-dismissed' }: StickyCtaBarProps) {\n  // Starts hidden so server and first client render agree — the scroll handler\n  // is the only thing that reveals it.\n  const [visible, setVisible] = useState(false)\n  const dismissed = useRef(false)\n\n  useEffect(() => {\n    try {\n      dismissed.current = window.sessionStorage.getItem(storageKey) === '1'\n    } catch {\n      dismissed.current = false\n    }\n\n    const onScroll = () => setVisible(!dismissed.current && window.scrollY > threshold)\n    onScroll()\n    window.addEventListener('scroll', onScroll, { passive: true })\n    return () => window.removeEventListener('scroll', onScroll)\n  }, [threshold, storageKey])\n\n  function dismiss() {\n    dismissed.current = true\n    setVisible(false)\n    try {\n      window.sessionStorage.setItem(storageKey, '1')\n    } catch {\n      // Private browsing or blocked storage: dismissing still holds for this page view.\n    }\n  }\n\n  return (\n    <div\n      data-slot=\"sticky-cta-bar\"\n      className={`fixed inset-x-0 bottom-0 z-50 px-4 pb-4 transition duration-200 ${\n        visible ? 'translate-y-0 opacity-100' : 'pointer-events-none invisible translate-y-full opacity-0'\n      }`}\n      role=\"region\"\n      aria-label=\"Trial offer\"\n      aria-hidden={!visible}\n    >\n      <div className=\"border-border bg-card/95 mx-auto flex max-w-4xl flex-wrap items-center gap-x-5 gap-y-3 rounded-xl border p-3 pl-4 shadow-lg backdrop-blur\">\n        <Badge variant=\"secondary\" className=\"gap-1.5\">\n          <Clock className=\"size-3\" aria-hidden=\"true\" />\n          Free for 14 days\n        </Badge>\n\n        <p className=\"min-w-0 grow text-sm\">\n          <span className=\"font-medium\">Start on the full plan.</span>\n          <span className=\"text-muted-foreground\"> No card, no sales call, cancel from the dashboard.</span>\n        </p>\n\n        <div className=\"flex items-center gap-2\">\n          <Button size=\"sm\">Start free trial</Button>\n          <Button size=\"sm\" variant=\"ghost\" className=\"hidden sm:inline-flex\">\n            Talk to us\n          </Button>\n          <Separator orientation=\"vertical\" className=\"hidden h-6 sm:block\" />\n          <Button size=\"icon\" variant=\"ghost\" aria-label=\"Dismiss this offer\" onClick={dismiss}>\n            <X className=\"size-4\" aria-hidden=\"true\" />\n          </Button>\n        </div>\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:block",
      "target": "~/components/blocks/sticky-cta/StickyCtaBar.tsx"
    },
    {
      "path": "packages/registry-react/blocks/sticky-cta/StickyCtaCornerButton.tsx",
      "content": "'use client'\n\nimport { useState } from 'react'\nimport { MessageSquare } from 'lucide-react'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\n\nexport function StickyCtaCornerButton() {\n  // Expands on focus as well as hover: a keyboard user should be able to read\n  // the label before deciding whether to activate it.\n  const [expanded, setExpanded] = useState(false)\n\n  return (\n    <div data-slot=\"sticky-cta-corner-button\" className=\"fixed right-5 bottom-5 z-50\">\n      <Button\n        size=\"lg\"\n        className=\"group h-12 gap-0 rounded-full pr-4 pl-4 shadow-lg\"\n        onMouseEnter={() => setExpanded(true)}\n        onMouseLeave={() => setExpanded(false)}\n        onFocus={() => setExpanded(true)}\n        onBlur={() => setExpanded(false)}\n      >\n        <MessageSquare className=\"size-5 shrink-0\" aria-hidden=\"true\" />\n        {/* Width, not display: a hidden label would not animate, and a label that\n            pops in shifts the button under the cursor mid-click. */}\n        <span\n          className={`overflow-hidden whitespace-nowrap transition-colors duration-200 ${\n            expanded ? 'ml-2 max-w-[10rem] opacity-100' : 'max-w-0 opacity-0'\n          }`}\n        >\n          Ask an engineer\n        </span>\n      </Button>\n\n      <Badge\n        variant=\"secondary\"\n        className=\"border-background pointer-events-none absolute -top-1 -right-1 size-5 justify-center rounded-full border-2 p-0 text-xs\"\n      >\n        1\n      </Badge>\n    </div>\n  )\n}\n",
      "type": "registry:block",
      "target": "~/components/blocks/sticky-cta/StickyCtaCornerButton.tsx"
    },
    {
      "path": "packages/registry-react/blocks/sticky-cta/StickyCtaReadingProgress.tsx",
      "content": "'use client'\n\nimport { useEffect, useRef, useState } from 'react'\nimport { ArrowRight, X } from 'lucide-react'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport { Progress } from '@/components/ui/progress'\nimport { Separator } from '@/components/ui/separator'\n\nexport function StickyCtaReadingProgress({ revealAt = 15 }: { revealAt?: number }) {\n  // Both start at rest so server and first client paint agree; the scroll\n  // handler is the only thing that reveals the bar or moves the progress.\n  const [progress, setProgress] = useState(0)\n  const [visible, setVisible] = useState(false)\n  const dismissed = useRef(false)\n\n  useEffect(() => {\n    const onScroll = () => {\n      const scrollable = document.documentElement.scrollHeight - window.innerHeight\n      const percent = scrollable > 0 ? Math.min(Math.round((window.scrollY / scrollable) * 100), 100) : 0\n      setProgress(percent)\n      setVisible(!dismissed.current && percent >= revealAt)\n    }\n    onScroll()\n    window.addEventListener('scroll', onScroll, { passive: true })\n    window.addEventListener('resize', onScroll)\n    return () => {\n      window.removeEventListener('scroll', onScroll)\n      window.removeEventListener('resize', onScroll)\n    }\n  }, [revealAt])\n\n  return (\n    <div\n      data-slot=\"sticky-cta-reading-progress\"\n      className={`border-border bg-card/95 fixed inset-x-0 bottom-0 z-50 border-t backdrop-blur transition duration-200 ${\n        visible ? 'translate-y-0 opacity-100' : 'pointer-events-none invisible translate-y-full opacity-0'\n      }`}\n      role=\"region\"\n      aria-label=\"Reading progress and trial offer\"\n      aria-hidden={!visible}\n    >\n      <Progress value={progress} className=\"h-0.5 rounded-none\" aria-label=\"Reading progress\" />\n\n      <div className=\"mx-auto flex max-w-5xl flex-wrap items-center gap-x-5 gap-y-2 px-6 py-3\">\n        <Badge variant=\"secondary\" className=\"shrink-0 tabular-nums\">\n          {progress}% read\n        </Badge>\n\n        <p className=\"min-w-0 grow text-sm\">\n          <span className=\"font-medium\">Finished the technical detail?</span>\n          <span className=\"text-muted-foreground\"> The trial runs on your real warehouse, read-only.</span>\n        </p>\n\n        <div className=\"flex shrink-0 items-center gap-2\">\n          <Button size=\"sm\">\n            Start free\n            <ArrowRight className=\"ml-1.5 size-3.5\" aria-hidden=\"true\" />\n          </Button>\n          <Separator orientation=\"vertical\" className=\"hidden h-5 sm:block\" />\n          <Button\n            size=\"icon\"\n            variant=\"ghost\"\n            className=\"size-7\"\n            aria-label=\"Dismiss\"\n            onClick={() => {\n              dismissed.current = true\n              setVisible(false)\n            }}\n          >\n            <X className=\"size-3.5\" aria-hidden=\"true\" />\n          </Button>\n        </div>\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:block",
      "target": "~/components/blocks/sticky-cta/StickyCtaReadingProgress.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/progress.json",
    "https://uipkge.dev/r/react/separator.json"
  ],
  "description": "Sticky conversion trio that stays out of the way until scrolled for: a bottom CTA bar with offer line and social proof, a compact corner button that expands on hover or focus, and a bottom bar that doubles as a reading-progress indicator.",
  "categories": [
    "marketing"
  ]
}