{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "sticky-cta-bar",
  "title": "Sticky CTA Bar",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-angular/blocks/sticky-cta-bar/sticky-cta-bar.component.ts",
      "content": "// Scroll-triggered bottom CTA bar that reveals past a threshold, carrying an offer\n// line, inline social proof, primary + ghost actions, and a dismiss control that\n// stays dismissed for the session.\n// Deprecated alias of the sticky conversion trio (use ui-sticky-cta-bar from `sticky-cta`).\n// Port of the Vue/React block 1:1.\nimport { ChangeDetectionStrategy, Component, Input, OnDestroy, OnInit, signal } from '@angular/core'\nimport { Clock, LucideAngularModule, X } from 'lucide-angular'\nimport { cn } from '@/lib/utils'\nimport { UiBadgeComponent } from '@/ui/badge/badge.component'\nimport { UiButtonComponent } from '@/ui/button/button.component'\nimport { UiSeparatorComponent } from '@/ui/separator/separator.component'\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-sticky-cta-bar, [ui-sticky-cta-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    <div\n      data-slot=\"sticky-cta-bar\"\n      [class]=\"rootClass\"\n      role=\"region\"\n      aria-label=\"Trial offer\"\n      [attr.aria-hidden]=\"!visible()\"\n    >\n      <div\n        class=\"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      >\n        <span ui-badge variant=\"secondary\" class=\"gap-1.5\">\n          <lucide-icon [img]=\"Clock\" class=\"size-3\" />\n          Free for 14 days\n        </span>\n\n        <p class=\"min-w-0 grow text-sm\">\n          <span class=\"font-medium\">Start on the full plan.</span>\n          <span class=\"text-muted-foreground\"> No card, no sales call, cancel from the dashboard.</span>\n        </p>\n\n        <div class=\"flex items-center gap-2\">\n          <button ui-button size=\"sm\">Start free trial</button>\n          <button ui-button size=\"sm\" variant=\"ghost\" class=\"hidden sm:inline-flex\">Talk to us</button>\n          <div ui-separator orientation=\"vertical\" class=\"hidden h-6 sm:block\"></div>\n          <button ui-button size=\"icon\" variant=\"ghost\" aria-label=\"Dismiss this offer\" (click)=\"dismiss()\">\n            <lucide-icon [img]=\"X\" class=\"size-4\" />\n          </button>\n        </div>\n      </div>\n    </div>\n  `,\n})\nexport class UiStickyCtaBarComponent implements OnInit, OnDestroy {\n  protected readonly Clock = Clock\n  protected readonly X = X\n\n  /** Scroll distance in pixels before the bar reveals. */\n  @Input() threshold = 480\n  /** sessionStorage key holding the dismissal, so it stays closed for the session. */\n  @Input() storageKey = 'uipkge:sticky-cta-dismissed'\n  @Input('class') className?: string\n\n  // Starts hidden so server and first client render agree — the scroll handler\n  // is the only thing that reveals it.\n  readonly visible = signal(false)\n  private dismissed = false\n  private onScrollBound: (() => void) | null = null\n\n  ngOnInit(): void {\n    if (typeof window === 'undefined') return\n    try {\n      this.dismissed = window.sessionStorage.getItem(this.storageKey) === '1'\n    } catch {\n      this.dismissed = false\n    }\n    this.onScrollBound = () => this.onScroll()\n    this.onScroll()\n    window.addEventListener('scroll', this.onScrollBound, { passive: true })\n  }\n\n  ngOnDestroy(): void {\n    if (typeof window !== 'undefined' && this.onScrollBound) {\n      window.removeEventListener('scroll', this.onScrollBound)\n    }\n  }\n\n  get rootClass(): string {\n    return cn(\n      'fixed inset-x-0 bottom-0 z-50 px-4 pb-4 transition duration-200',\n      this.visible() ? 'translate-y-0 opacity-100' : 'pointer-events-none invisible translate-y-full opacity-0',\n      this.className,\n    )\n  }\n\n  onScroll(): void {\n    this.visible.set(!this.dismissed && window.scrollY > this.threshold)\n  }\n\n  dismiss(): void {\n    this.dismissed = true\n    this.visible.set(false)\n    try {\n      window.sessionStorage.setItem(this.storageKey, '1')\n    } catch {\n      // Private browsing or blocked storage: dismissing still holds for this page view.\n    }\n  }\n}\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/sticky-cta-bar/sticky-cta-bar.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/sticky-cta-bar/index.ts",
      "content": "export { UiStickyCtaBarComponent } from './sticky-cta-bar.component'\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/sticky-cta-bar/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/separator.json"
  ],
  "description": "Scroll-triggered bottom CTA bar that reveals past a threshold, carrying an offer line, inline social proof, primary + ghost actions, and a dismiss control that stays dismissed for the session.",
  "categories": [
    "marketing"
  ],
  "deprecated": true,
  "replacedBy": "sticky-cta"
}