{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "newsletter",
  "title": "Newsletter",
  "type": "registry:block",
  "description": "Newsletter growth quartet: a slim inline signup band, a filterable issue archive with featured spotlight, centered and split signup forms, and a split section pairing the pitch with a preview of the last issue.",
  "categories": [
    "marketing",
    "media",
    "app"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/blocks/newsletter/newsletter.ts",
      "content": "import { LitElement, css, html, nothing } from 'lit'\nimport {\n  ArrowRight,\n  BellRing,\n  BookOpen,\n  Bookmark,\n  Calendar,\n  Check,\n  Clock,\n  Heart,\n  Mail,\n  MessageSquare,\n  Newspaper,\n  Rss,\n  Search,\n  Send,\n  Users,\n  X,\n  Zap,\n} from 'lucide'\nimport { cn } from '../../lib/utils'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\nimport { icon } from '../../lib/icon'\n// Side-effect imports: register the composed elements for standalone use\n// (consumers who install only this block still get working primitives).\nimport '../../components/badge/badge'\nimport '../../components/button/button'\nimport '../../components/card/card'\nimport '../../components/input/input'\nimport type { UipInput } from '../../components/input/input'\nimport '../../components/separator/separator'\n\nexport type NewsletterVariant = 'inline-band' | 'archive' | 'signup-centered' | 'signup-split' | 'split-preview'\n\ninterface ChannelOption {\n  id: string\n  title: string\n  frequency: string\n  description: string\n  badge: string\n}\n\nconst channels: ChannelOption[] = [\n  {\n    id: 'architecture',\n    title: 'Architecture & Token Deep Dives',\n    frequency: 'Bi-Weekly',\n    description: 'Technical breakdowns on OKLCH color science, headless AST transformers, and zero-lockin paradigms.',\n    badge: 'Flagship',\n  },\n  {\n    id: 'releases',\n    title: 'Registry Ship Notes & RFCs',\n    frequency: 'Monthly',\n    description: 'Direct changelogs, new workbench releases, breaking primitive refactors, and roadmap discussions.',\n    badge: 'Changelog',\n  },\n  {\n    id: 'security',\n    title: 'Security & Dependency Bulletins',\n    frequency: 'As-Needed',\n    description: 'Zero-day vulnerability alerts, upstream headless primitive fixes, and patch notifications.',\n    badge: 'Critical',\n  },\n]\n\nconst signupIssues = [\n  {\n    id: 48,\n    date: 'Aug 2026',\n    title: 'The Death of Monolithic npm UI Packages',\n    reads: '4 min read',\n    tags: ['Architecture', 'Registry', 'AST'],\n    snippet:\n      'Why the next decade of frontend engineering belongs to unbundled registries where the consumer owns the source code.',\n  },\n  {\n    id: 47,\n    date: 'Jul 2026',\n    title: 'OKLCH Theming in Tailwind CSS v4',\n    reads: '6 min read',\n    tags: ['Tailwind v4', 'Color Science', 'Tokens'],\n    snippet:\n      'Mastering perceptual lightness, chroma gamut mapping, and dynamic theme switching without runtime CSS bloat.',\n  },\n]\n\nconst lastIssue = {\n  subject: 'Why we version metric definitions instead of dashboards',\n  date: 'Feb 18, 2026',\n  readTime: '9 min',\n  opening:\n    'Dashboards are the output. The definition is the thing teams argue about, so that is what belongs under review — with a diff, an owner, and a revert path. This issue walks through what changed when we moved ours into the repo.',\n}\n\nconst archiveCategories = ['All Issues', 'Architecture', 'Design Tokens', 'Performance', 'Interviews'] as const\n\ninterface ArchiveIssue {\n  id: string\n  issueNumber: number\n  volumeDate: string\n  title: string\n  excerpt: string\n  category: 'Architecture' | 'Design Tokens' | 'Performance' | 'Interviews'\n  tags: string[]\n  readTime: string\n  date: string\n  commentsCount: number\n  likesCount: number\n  featured?: boolean\n}\n\nconst featuredIssue: ArchiveIssue = {\n  id: 'issue-43',\n  issueNumber: 43,\n  volumeDate: 'Aug 2026',\n  title: 'The Death of npm Component Libraries: Why Unbundled Registry Architecture Won',\n  excerpt:\n    'How unbundled UI registries replaced monolithic node_modules packages, eliminated dependency hell, and gave full code ownership back to frontend engineering teams across the industry.',\n  category: 'Architecture',\n  tags: ['#architecture', '#registries', '#future-of-web', '#tooling'],\n  readTime: '9 min read',\n  date: 'Aug 25, 2026',\n  commentsCount: 128,\n  likesCount: 1420,\n  featured: true,\n}\n\nconst archiveIssues: ArchiveIssue[] = [\n  {\n    id: 'issue-42',\n    issueNumber: 42,\n    volumeDate: 'Aug 2026',\n    title: 'Why Zero-Dependency Registries Are Winning',\n    excerpt:\n      'A deep dive into why enterprise engineering teams are abandoning monolithic UI packages in favor of composable own-your-code registry models that eliminate breaking upgrade cascades.',\n    category: 'Architecture',\n    tags: ['#architecture', '#web-performance', '#dx'],\n    readTime: '6 min read',\n    date: 'Aug 18, 2026',\n    commentsCount: 84,\n    likesCount: 642,\n  },\n  {\n    id: 'issue-41',\n    issueNumber: 41,\n    volumeDate: 'Aug 2026',\n    title: 'Mastering Tailwind v4: OKLCH Colors & Dynamic Themes',\n    excerpt:\n      'How modern CSS color spaces and inline theme definitions unlock mathematically perceptually uniform light/dark transitions without CSS variables explosion.',\n    category: 'Design Tokens',\n    tags: ['#design-tokens', '#css', '#theming'],\n    readTime: '8 min read',\n    date: 'Aug 11, 2026',\n    commentsCount: 56,\n    likesCount: 519,\n  },\n  {\n    id: 'issue-40',\n    issueNumber: 40,\n    volumeDate: 'Aug 2026',\n    title: 'Zero-CLS Island Hydration in Modern Web Frameworks',\n    excerpt:\n      'Eliminating Cumulative Layout Shift when server-rendered islands hydrate in Astro and Nuxt. Practical patterns distilled from serving 1M+ monthly pageviews.',\n    category: 'Performance',\n    tags: ['#performance', '#core-web-vitals', '#astro'],\n    readTime: '5 min read',\n    date: 'Aug 04, 2026',\n    commentsCount: 92,\n    likesCount: 730,\n  },\n  {\n    id: 'issue-39',\n    issueNumber: 39,\n    volumeDate: 'Jul 2026',\n    title: 'Interview: Building High-Craft Interfaces with Paco Coursey',\n    excerpt:\n      'The creator of cmdk and sonner breaks down spring physics, popover placement math, and why 60fps micro-interactions define software brand trust.',\n    category: 'Interviews',\n    tags: ['#interviews', '#craft', '#animation'],\n    readTime: '11 min read',\n    date: 'Jul 28, 2026',\n    commentsCount: 147,\n    likesCount: 1205,\n  },\n  {\n    id: 'issue-38',\n    issueNumber: 38,\n    volumeDate: 'Jul 2026',\n    title: 'Building Resilient Component APIs with Polymorphic Slots',\n    excerpt:\n      'Why the asChild composition pattern beat standard prop drilling for accessible primitives, and how Reka UI implements headless polymorphism without DOM overhead.',\n    category: 'Architecture',\n    tags: ['#architecture', '#vue', '#reka-ui'],\n    readTime: '7 min read',\n    date: 'Jul 21, 2026',\n    commentsCount: 63,\n    likesCount: 488,\n  },\n  {\n    id: 'issue-37',\n    issueNumber: 37,\n    volumeDate: 'Jul 2026',\n    title: 'Designing for Multi-Tenant White-Labeling at Scale',\n    excerpt:\n      'Architecting a headless token graph that dynamically maps corporate brand identities and custom color palettes across 500+ enterprise subdomains in real time.',\n    category: 'Design Tokens',\n    tags: ['#design-tokens', '#architecture', '#enterprise'],\n    readTime: '9 min read',\n    date: 'Jul 14, 2026',\n    commentsCount: 71,\n    likesCount: 610,\n  },\n]\n\n/**\n * <uip-newsletter> — the registry Newsletter block collection as a web component.\n *\n * One block, one install unit: `variant` selects the section ('inline-band' |\n * 'archive' | 'signup-centered' | 'signup-split' | 'split-preview'); the\n * archive takes `publication-name`, `subtitle` and `subscriber-count`.\n * Class strings are React's verbatim per file. React state (useState) becomes\n * @state fields; every form dispatches a `subscribe` CustomEvent (detail:\n * { email }, bubbles, composed) — React has no submit callback. The search\n * field's prefix icon moves into <uip-input>'s prefix slot (same visual\n * result, no overlay positioning to maintain).\n *\n * NOTE: React's `variant` prop on NewsletterSignup ('centered' | 'split')\n * never changes its output — both stories render the same channel-selector\n * layout. Preserved here 1:1.\n */\nexport class UipNewsletter extends LitElement {\n  static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true }\n  // :host display can't be a utility class (the host has no template of its own).\n  static styles = [tailwind, css`:host { display: block; }`]\n\n  static properties = {\n    variant: { reflect: true },\n    publicationName: { attribute: 'publication-name' },\n    subtitle: {},\n    subscriberCount: { attribute: 'subscriber-count' },\n    inlineEmail: { state: true },\n    inlineDone: { state: true },\n    archiveEmail: { state: true },\n    archiveDone: { state: true },\n    archiveQuery: { state: true },\n    archiveCategory: { state: true },\n    signupEmail: { state: true },\n    signupChannel: { state: true },\n    signupDone: { state: true },\n    signupPreview: { state: true },\n    signupSubmitting: { state: true },\n    previewEmail: { state: true },\n    previewDone: { state: true },\n  }\n\n  variant: NewsletterVariant = 'inline-band'\n  publicationName = 'The Unbundled Engineer'\n  subtitle = 'Weekly architectural teardowns of modern design systems, web performance, and component registries.'\n  subscriberCount = '42,500+ Subscribers · Top 1% on Substack'\n  private inlineEmail = ''\n  private inlineDone = false\n  private archiveEmail = ''\n  private archiveDone = false\n  private archiveQuery = ''\n  private archiveCategory = 'All Issues'\n  private signupEmail = ''\n  private signupChannel = 'architecture'\n  private signupDone = false\n  private signupPreview = 0\n  private signupSubmitting = false\n  private previewEmail = ''\n  private previewDone = false\n  private timer?: number\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'newsletter')\n  }\n\n  disconnectedCallback() {\n    super.disconnectedCallback()\n    if (this.timer) window.clearTimeout(this.timer)\n  }\n\n  private emitSubscribe(email: string) {\n    this.dispatchEvent(new CustomEvent('subscribe', { detail: { email }, bubbles: true, composed: true }))\n  }\n\n  private get filteredIssues() {\n    const query = this.archiveQuery.trim().toLowerCase()\n    return archiveIssues.filter((issue) => {\n      const matchesCategory = this.archiveCategory === 'All Issues' || issue.category === this.archiveCategory\n      const matchesSearch =\n        !query ||\n        issue.title.toLowerCase().includes(query) ||\n        issue.excerpt.toLowerCase().includes(query) ||\n        issue.tags.some((tag) => tag.toLowerCase().includes(query)) ||\n        `issue #${issue.issueNumber}`.toLowerCase().includes(query)\n      return matchesCategory && matchesSearch\n    })\n  }\n\n  private categoryCount(cat: string) {\n    if (cat === 'All Issues') return archiveIssues.length\n    return archiveIssues.filter((issue) => issue.category === cat).length\n  }\n\n  private clearArchiveFilters() {\n    this.archiveQuery = ''\n    this.archiveCategory = 'All Issues'\n  }\n\n  render() {\n    switch (this.variant) {\n      case 'archive':\n        return this.renderArchive()\n      case 'signup-centered':\n      case 'signup-split':\n        return this.renderSignup()\n      case 'split-preview':\n        return this.renderSplitPreview()\n      default:\n        return this.renderInlineBand()\n    }\n  }\n\n  private renderInlineBand() {\n    return html`\n      <section data-slot=\"newsletter-inline-band\" class=\"border-border bg-muted/40 border-y\">\n        <div class=\"mx-auto flex max-w-6xl flex-col gap-6 px-6 py-10 lg:flex-row lg:items-center lg:gap-12\">\n          <div class=\"min-w-0 lg:flex-1\">\n            <div class=\"flex items-center gap-2\">\n              ${icon(Mail, 'mail', 'text-muted-foreground size-4')}\n              <uip-badge variant=\"secondary\">Monthly</uip-badge>\n            </div>\n            <p class=\"mt-2.5 text-lg leading-snug font-medium text-balance\">\n              Engineering notes on metric modelling, access control, and warehouse cost.\n            </p>\n            <p class=\"text-muted-foreground mt-1.5 text-sm\">\n              Read by 8,400 data and finance engineers. One email a month, no product announcements.\n            </p>\n          </div>\n\n          <form\n            class=\"lg:w-[26rem] lg:shrink-0\"\n            @submit=${(e: Event) => {\n              e.preventDefault()\n              if (!this.inlineEmail.trim()) return\n              this.inlineDone = true\n              this.emitSubscribe(this.inlineEmail)\n            }}\n          >\n            <div class=\"flex gap-2\">\n              <uip-input\n                .value=${this.inlineEmail}\n                type=\"email\"\n                required\n                placeholder=\"you@company.com\"\n                autocomplete=\"email\"\n                aria-label=\"Email address\"\n                @input=${(e: Event) => {\n                  this.inlineEmail = (e.target as UipInput).value\n                }}\n              ></uip-input>\n              <uip-button type=\"submit\" class=\"shrink-0\">Subscribe</uip-button>\n            </div>\n            <p class=\"mt-2 min-h-5 text-xs\" aria-live=\"polite\">\n              ${this.inlineDone\n                ? html`<span class=\"text-success inline-flex items-center gap-1.5\">\n                    ${icon(Check, 'check', 'size-3')} Check your inbox to confirm.\n                  </span>`\n                : html`<span class=\"text-muted-foreground\">Unsubscribe in one click. We never share the list.</span>`}\n            </p>\n          </form>\n        </div>\n      </section>\n    `\n  }\n\n  private renderSignup() {\n    return html`\n      <section\n        data-slot=\"newsletter-signup\"\n        class=\"bg-background relative overflow-hidden px-4 py-16 sm:px-6 lg:px-8\"\n      >\n        <div class=\"mx-auto max-w-6xl space-y-10\">\n          <div class=\"max-w-3xl space-y-3\">\n            <uip-badge variant=\"secondary\" class=\"gap-1.5 px-3 py-1 font-mono text-xs shadow-xs\">\n              ${icon(Mail, 'mail', 'text-primary size-3.5')} Engineering Dispatch & Architecture Radar\n            </uip-badge>\n            <h2 class=\"text-foreground text-3xl font-bold tracking-tight sm:text-4xl\">\n              Ship notes, not marketing fluff.\n            </h2>\n            <p class=\"text-muted-foreground text-sm leading-relaxed sm:text-base\">\n              Join 18,400+ frontend architects, design engineers, and systems builders. Plain-text insights into\n              component architecture, token systems, and dual-framework engineering.\n            </p>\n          </div>\n\n          <div class=\"grid grid-cols-1 items-start gap-8 lg:grid-cols-12\">\n            <div class=\"space-y-6 lg:col-span-7\">\n              <uip-card class=\"border-border bg-card overflow-hidden shadow-sm\">\n                <uip-card-header class=\"bg-muted/20 border-border/60 border-b pb-4\">\n                  <div class=\"flex items-center justify-between\">\n                    <uip-card-title class=\"flex items-center gap-2 text-base font-semibold\">\n                      ${icon(BellRing, 'bell-ring', 'text-primary size-4')} Select Dispatch Track\n                    </uip-card-title>\n                    <span class=\"text-muted-foreground font-mono text-xs\">Zero Spam Guarantee</span>\n                  </div>\n                  <uip-card-description class=\"text-xs\">\n                    Choose the telemetry streams you want delivered to your inbox.\n                  </uip-card-description>\n                </uip-card-header>\n\n                <uip-card-content class=\"space-y-5 p-6\">\n                  <div class=\"space-y-2.5\">\n                    ${channels.map((ch) => {\n                      const isSelected = this.signupChannel === ch.id\n                      return html`\n                        <div\n                          class=${cn(\n                            'group cursor-pointer rounded-xl border p-3.5 transition-colors',\n                            isSelected\n                              ? 'border-primary bg-primary/5 ring-primary/20 shadow-xs ring-1'\n                              : 'border-border bg-background hover:bg-muted/40',\n                          )}\n                          @click=${() => {\n                            this.signupChannel = ch.id\n                          }}\n                        >\n                          <div class=\"flex items-center justify-between gap-2\">\n                            <div class=\"flex items-center gap-2\">\n                              <div\n                                class=${cn(\n                                  'flex size-3.5 items-center justify-center rounded-full border transition-colors',\n                                  isSelected ? 'border-primary bg-primary' : 'border-muted-foreground/40',\n                                )}\n                              >\n                                ${isSelected\n                                  ? html`<div class=\"bg-primary-foreground size-1.5 rounded-full\"></div>`\n                                  : nothing}\n                              </div>\n                              <span class=\"text-foreground text-xs font-semibold\">${ch.title}</span>\n                            </div>\n                            <uip-badge variant=\"outline\" class=\"font-mono text-xs\">${ch.frequency}</uip-badge>\n                          </div>\n                          <p class=\"text-muted-foreground mt-1.5 pl-5.5 text-xs leading-relaxed\">${ch.description}</p>\n                        </div>\n                      `\n                    })}\n                  </div>\n\n                  <uip-separator></uip-separator>\n\n                  ${!this.signupDone\n                    ? html`\n                        <form\n                          class=\"space-y-3\"\n                          @submit=${(e: Event) => {\n                            e.preventDefault()\n                            if (!this.signupEmail || !this.signupEmail.includes('@')) return\n                            this.signupSubmitting = true\n                            const email = this.signupEmail\n                            this.timer = window.setTimeout(() => {\n                              this.signupSubmitting = false\n                              this.signupDone = true\n                              this.emitSubscribe(email)\n                            }, 400)\n                          }}\n                        >\n                          <div class=\"flex flex-col gap-2 sm:flex-row\">\n                            <uip-input\n                              type=\"email\"\n                              placeholder=\"architect@company.com\"\n                              .value=${this.signupEmail}\n                              required\n                              class=\"h-10 flex-1 font-mono text-xs\"\n                              aria-label=\"Email address for dispatch\"\n                              @input=${(e: Event) => {\n                                this.signupEmail = (e.target as UipInput).value\n                              }}\n                            ></uip-input>\n                            <uip-button\n                              type=\"submit\"\n                              ?disabled=${this.signupSubmitting}\n                              class=\"h-10 gap-1.5 px-5 text-xs font-semibold\"\n                            >\n                              <span>Subscribe to Stream</span>\n                              ${icon(Send, 'send', 'size-3.5')}\n                            </uip-button>\n                          </div>\n                          <div class=\"text-muted-foreground flex items-center justify-between pt-1 font-mono text-xs\">\n                            <span>Plain text &bull; 1-click unsubscribe</span>\n                            <span class=\"text-success font-semibold\">18,412 Active Subscribers</span>\n                          </div>\n                        </form>\n                      `\n                    : html`\n                        <div class=\"border-success/20 bg-success/10 space-y-2 rounded-xl border p-5 text-center\">\n                          <div class=\"bg-success/20 text-success mx-auto flex size-9 items-center justify-center rounded-full\">\n                            ${icon(Check, 'check', 'size-5')}\n                          </div>\n                          <p class=\"text-foreground text-sm font-semibold\">Dispatches Activated!</p>\n                          <p class=\"text-muted-foreground text-xs\">\n                            Verification sent to <strong class=\"text-foreground font-mono\">${this.signupEmail}</strong>.\n                            Check your inbox to confirm delivery.\n                          </p>\n                        </div>\n                      `}\n                </uip-card-content>\n              </uip-card>\n            </div>\n\n            <div class=\"space-y-4 lg:col-span-5\">\n              <div class=\"flex items-center justify-between\">\n                <p class=\"text-muted-foreground flex items-center gap-1.5 font-mono text-xs tracking-wider uppercase\">\n                  ${icon(BookOpen, 'book-open', 'text-primary size-3.5')} Archive Sampler\n                </p>\n                <span class=\"text-muted-foreground font-mono text-xs\">\n                  Issue #${signupIssues[this.signupPreview].id}\n                </span>\n              </div>\n\n              <div class=\"space-y-3\">\n                ${signupIssues.map(\n                  (issue, idx) => html`\n                    <div\n                      class=${cn(\n                        'bg-card cursor-pointer rounded-xl border p-4 transition-colors',\n                        this.signupPreview === idx\n                          ? 'border-primary/50 shadow-xs'\n                          : 'border-border hover:border-border/80',\n                      )}\n                      @click=${() => {\n                        this.signupPreview = idx\n                      }}\n                    >\n                      <div class=\"text-muted-foreground mb-1.5 flex items-center justify-between font-mono text-xs\">\n                        <span> ${issue.date} &bull; Issue #${issue.id} </span>\n                        <span class=\"text-foreground font-medium\">${issue.reads}</span>\n                      </div>\n                      <h3 class=\"text-foreground mb-1.5 text-sm leading-snug font-bold\">${issue.title}</h3>\n                      <p class=\"text-muted-foreground text-xs leading-relaxed\">${issue.snippet}</p>\n\n                      <div class=\"mt-3 flex flex-wrap gap-1.5\">\n                        ${issue.tags.map(\n                          (tag) => html`<uip-badge variant=\"secondary\" class=\"px-1.5 py-0 font-mono text-xs\">\n                            #${tag}\n                          </uip-badge>`,\n                        )}\n                      </div>\n                    </div>\n                  `,\n                )}\n              </div>\n\n              <div\n                class=\"border-border bg-muted/20 text-muted-foreground flex items-center justify-between rounded-lg border p-3.5 text-xs\"\n              >\n                <span class=\"font-mono\">Prefer RSS feeds?</span>\n                <a\n                  href=\"https://uipkge.dev/rss.xml\"\n                  target=\"_blank\"\n                  rel=\"noreferrer\"\n                  class=\"text-primary inline-flex items-center gap-1 font-mono hover:underline\"\n                >\n                  uipkge.dev/rss.xml ${icon(ArrowRight, 'arrow-right', 'size-3')}\n                </a>\n              </div>\n            </div>\n          </div>\n        </div>\n      </section>\n    `\n  }\n\n  private renderSplitPreview() {\n    return html`\n      <section data-slot=\"newsletter-split-preview\" class=\"bg-background\">\n        <div class=\"mx-auto max-w-6xl px-6 py-20 lg:py-28\">\n          <div class=\"grid items-center gap-10 lg:grid-cols-2 lg:gap-16\">\n            <div>\n              <uip-badge variant=\"secondary\" class=\"gap-1.5\">\n                ${icon(Mail, 'mail', 'size-3')} Monthly\n              </uip-badge>\n              <h2 class=\"mt-4 text-3xl font-semibold tracking-tight text-balance sm:text-4xl\">\n                Notes from the people who build it\n              </h2>\n              <p class=\"text-muted-foreground mt-3 text-lg\">\n                One email a month on metric modelling, access control, and warehouse cost. No product announcements.\n              </p>\n\n              <form\n                class=\"mt-6 max-w-md\"\n                @submit=${(e: Event) => {\n                  e.preventDefault()\n                  if (!this.previewEmail.trim()) return\n                  this.previewDone = true\n                  this.emitSubscribe(this.previewEmail)\n                }}\n              >\n                <div class=\"flex gap-2\">\n                  <uip-input\n                    .value=${this.previewEmail}\n                    type=\"email\"\n                    required\n                    placeholder=\"you@company.com\"\n                    autocomplete=\"email\"\n                    aria-label=\"Email address\"\n                    @input=${(e: Event) => {\n                      this.previewEmail = (e.target as UipInput).value\n                    }}\n                  ></uip-input>\n                  <uip-button type=\"submit\" class=\"shrink-0\">Subscribe</uip-button>\n                </div>\n                <p class=\"mt-2 min-h-5 text-xs\" aria-live=\"polite\">\n                  ${this.previewDone\n                    ? html`<span class=\"text-success inline-flex items-center gap-1.5\">\n                        ${icon(Check, 'check', 'size-3')} Check your inbox to confirm.\n                      </span>`\n                    : html`<span class=\"text-muted-foreground\"\n                        >8,400 subscribers · unsubscribe in one click.</span\n                      >`}\n                </p>\n              </form>\n            </div>\n\n            <uip-card>\n              <uip-card-content class=\"p-6\">\n                <div class=\"flex flex-wrap items-center gap-3\">\n                  <uip-badge variant=\"outline\">Last issue</uip-badge>\n                  <span class=\"text-muted-foreground text-xs\"> ${lastIssue.date} · ${lastIssue.readTime} </span>\n                </div>\n\n                <h3 class=\"mt-4 text-lg leading-snug font-semibold tracking-tight text-balance\">${lastIssue.subject}</h3>\n\n                <uip-separator class=\"[&::part(base)]:my-4\"></uip-separator>\n\n                <p class=\"text-muted-foreground text-sm leading-relaxed\">${lastIssue.opening}</p>\n\n                <uip-button variant=\"link\" class=\"mt-4 h-auto p-0 text-sm\">Read the full issue</uip-button>\n              </uip-card-content>\n            </uip-card>\n          </div>\n        </div>\n      </section>\n    `\n  }\n\n  private renderArchive() {\n    const filtered = this.filteredIssues\n    return html`\n      <div data-slot=\"newsletter-issue-archive\" class=\"mx-auto w-full max-w-5xl space-y-12 px-4 py-8 sm:px-6 sm:py-12\">\n        <header class=\"mx-auto max-w-3xl space-y-4 text-center\">\n          <div\n            class=\"border-primary/20 bg-primary/5 text-primary inline-flex items-center gap-1.5 rounded-full border px-3.5 py-1 text-xs font-medium shadow-xs\"\n          >\n            ${icon(Users, 'users', 'size-3.5 shrink-0')}\n            <span>${this.subscriberCount}</span>\n          </div>\n\n          <h1 class=\"text-foreground text-3xl font-bold tracking-tight sm:text-4xl lg:text-5xl\">\n            ${this.publicationName}\n          </h1>\n\n          <p class=\"text-muted-foreground mx-auto max-w-2xl text-base leading-relaxed sm:text-lg\">${this.subtitle}</p>\n\n          <div class=\"mx-auto w-full max-w-md pt-2\">\n            ${!this.archiveDone\n              ? html`\n                  <div>\n                    <form\n                      class=\"flex flex-col gap-2 sm:flex-row sm:gap-0\"\n                      @submit=${(e: Event) => {\n                        e.preventDefault()\n                        if (!this.archiveEmail || !this.archiveEmail.includes('@')) return\n                        this.archiveDone = true\n                        this.emitSubscribe(this.archiveEmail)\n                      }}\n                    >\n                      <uip-input\n                        id=\"newsletter-archive-email-lit\"\n                        .value=${this.archiveEmail}\n                        type=\"email\"\n                        placeholder=\"Enter your work email...\"\n                        required\n                        class=\"bg-card h-11 text-sm shadow-xs sm:rounded-r-none sm:[&::part(control)]:rounded-r-none\"\n                        aria-label=\"Work email address\"\n                        @input=${(e: Event) => {\n                          this.archiveEmail = (e.target as UipInput).value\n                        }}\n                      ></uip-input>\n                      <uip-button\n                        type=\"submit\"\n                        class=\"h-11 shrink-0 px-6 font-medium sm:rounded-l-none sm:[&::part(base)]:rounded-l-none\"\n                      >\n                        Subscribe for Free\n                      </uip-button>\n                    </form>\n                    <p\n                      class=\"text-muted-foreground mt-2.5 flex flex-wrap items-center justify-center gap-x-2 gap-y-1 text-xs\"\n                    >\n                      <span>Free weekly issues</span>\n                      <span>Zero spam</span>\n                      <span>1-click unsubscribe</span>\n                    </p>\n                  </div>\n                `\n              : html`\n                  <div\n                    class=\"border-success/30 bg-success/10 text-success inline-flex items-center gap-2.5 rounded-lg border px-4 py-3 text-sm font-medium shadow-xs\"\n                  >\n                    ${icon(Check, 'check', 'size-4 shrink-0')}\n                    <span>You&rsquo;re subscribed! Check your inbox for confirmation.</span>\n                  </div>\n                `}\n          </div>\n        </header>\n\n        <section aria-labelledby=\"featured-issue-heading-lit\" class=\"space-y-3\">\n          <div class=\"flex items-center justify-between px-1\">\n            <h2\n              id=\"featured-issue-heading-lit\"\n              class=\"text-muted-foreground text-xs font-semibold tracking-wider uppercase\"\n            >\n              Pinned Spotlight Edition\n            </h2>\n            <uip-badge variant=\"outline\" class=\"text-primary border-primary/30 gap-1 text-xs\">\n              ${icon(Zap, 'zap', 'size-3')} Latest Release\n            </uip-badge>\n          </div>\n\n          <uip-card class=\"border-border/80 bg-card hover:border-primary/40 overflow-hidden shadow-xs transition-colors\">\n            <div class=\"grid grid-cols-1 gap-6 p-6 sm:p-8 lg:grid-cols-12\">\n              <div class=\"flex flex-col justify-between space-y-4 lg:col-span-7\">\n                <div class=\"space-y-3\">\n                  <div class=\"flex flex-wrap items-center gap-2\">\n                    <uip-badge variant=\"default\" class=\"font-mono text-xs\">\n                      Issue #${featuredIssue.issueNumber} · ${featuredIssue.volumeDate}\n                    </uip-badge>\n                    <uip-badge variant=\"secondary\" class=\"text-xs\">${featuredIssue.category}</uip-badge>\n                    <span class=\"text-muted-foreground ml-auto inline-flex items-center gap-1 text-xs\">\n                      ${icon(Clock, 'clock', 'size-3')} ${featuredIssue.readTime}\n                    </span>\n                  </div>\n\n                  <h3\n                    class=\"text-foreground hover:text-primary cursor-pointer text-xl font-bold tracking-tight transition-colors sm:text-2xl\"\n                  >\n                    ${featuredIssue.title}\n                  </h3>\n\n                  <p class=\"text-muted-foreground text-sm leading-relaxed sm:text-base\">${featuredIssue.excerpt}</p>\n                </div>\n\n                <div class=\"space-y-4 pt-2\">\n                  <div class=\"flex flex-wrap gap-1.5\">\n                    ${featuredIssue.tags.map(\n                      (tag) => html`<uip-badge\n                        variant=\"outline\"\n                        class=\"text-muted-foreground font-mono text-xs font-normal\"\n                      >\n                        ${tag}\n                      </uip-badge>`,\n                    )}\n                  </div>\n\n                  <div class=\"border-border/60 flex flex-wrap items-center justify-between gap-4 border-t pt-3\">\n                    <div class=\"text-muted-foreground flex items-center gap-4 text-xs\">\n                      <span class=\"inline-flex items-center gap-1.5\">\n                        ${icon(Calendar, 'calendar', 'size-3.5')} ${featuredIssue.date}\n                      </span>\n                      <span class=\"inline-flex items-center gap-1.5\">\n                        ${icon(MessageSquare, 'message-square', 'size-3.5')} ${featuredIssue.commentsCount} comments\n                      </span>\n                      <span class=\"inline-flex items-center gap-1.5\">\n                        ${icon(Heart, 'heart', 'size-3.5')} ${featuredIssue.likesCount}\n                      </span>\n                    </div>\n\n                    <uip-button variant=\"default\" size=\"sm\" class=\"group gap-1.5 font-medium\">\n                      Read Issue\n                      ${icon(ArrowRight, 'arrow-right', 'size-3.5 transition-transform group-hover:translate-x-0.5')}\n                    </uip-button>\n                  </div>\n                </div>\n              </div>\n\n              <div class=\"flex items-center lg:col-span-5\">\n                <div class=\"border-border/80 bg-muted/40 w-full rounded-lg border p-4 font-mono text-xs sm:p-5\">\n                  <div class=\"border-border/60 mb-3 flex items-center justify-between border-b pb-2.5\">\n                    <div class=\"flex items-center gap-1.5\">\n                      <span class=\"bg-destructive/60 inline-block size-2.5 rounded-full\"></span>\n                      <span class=\"bg-warning/60 inline-block size-2.5 rounded-full\"></span>\n                      <span class=\"bg-success/60 inline-block size-2.5 rounded-full\"></span>\n                    </div>\n                    <span class=\"text-muted-foreground text-xs\">registry-teardown.ts</span>\n                  </div>\n                  <div class=\"text-muted-foreground space-y-2\">\n                    <div class=\"text-primary text-xs font-medium\">// Own your component code</div>\n                    <div><span class=\"text-foreground\">$</span> npx shadcn@latest add @uipkge/card</div>\n                    <div class=\"text-success\">✔ Fetched raw TypeScript TSX</div>\n                    <div class=\"text-success\">✔ Merged local Tailwind v4 tokens</div>\n                    <div class=\"text-muted-foreground/80 pt-1\">// Zero runtime package lock-in</div>\n                  </div>\n                  <div class=\"border-border/40 mt-4 flex flex-wrap gap-1.5 border-t pt-3\">\n                    <span class=\"bg-background text-foreground border-border rounded border px-2 py-0.5 text-xs\">\n                      Zero npm deps\n                    </span>\n                    <span class=\"bg-background text-foreground border-border rounded border px-2 py-0.5 text-xs\">\n                      Tailwind v4\n                    </span>\n                    <span class=\"bg-background text-foreground border-border rounded border px-2 py-0.5 text-xs\">\n                      OKLCH\n                    </span>\n                  </div>\n                </div>\n              </div>\n            </div>\n          </uip-card>\n        </section>\n\n        <section aria-label=\"Search and filter archive\" class=\"space-y-4\">\n          <div class=\"flex flex-col items-stretch justify-between gap-4 md:flex-row md:items-center\">\n            <div class=\"w-full md:max-w-md\">\n              <uip-input\n                .value=${this.archiveQuery}\n                type=\"search\"\n                placeholder=\"Search issues by title, topic, or tag...\"\n                allow-clear\n                class=\"bg-card shadow-xs\"\n                aria-label=\"Search archive issues\"\n                @input=${(e: Event) => {\n                  this.archiveQuery = (e.target as UipInput).value\n                }}\n              >\n                <span slot=\"prefix\">${icon(Search, 'search', 'size-4')}</span>\n              </uip-input>\n            </div>\n\n            <div class=\"text-muted-foreground flex items-center justify-between gap-3 text-xs md:justify-end\">\n              <span> Showing ${filtered.length} ${filtered.length === 1 ? 'edition' : 'editions'} </span>\n              ${this.archiveQuery || this.archiveCategory !== 'All Issues'\n                ? html`<button\n                    type=\"button\"\n                    class=\"text-primary focus-visible:ring-ring inline-flex items-center gap-1 rounded font-medium hover:underline focus-visible:ring-1 focus-visible:outline-none\"\n                    @click=${this.clearArchiveFilters}\n                  >\n                    Reset filters ${icon(X, 'x', 'size-3')}\n                  </button>`\n                : nothing}\n            </div>\n          </div>\n\n          <div class=\"flex flex-wrap items-center gap-1.5\" role=\"tablist\" aria-label=\"Filter by category\">\n            ${archiveCategories.map((cat) => {\n              const isSelected = this.archiveCategory === cat\n              return html`\n                <button\n                  type=\"button\"\n                  role=\"tab\"\n                  aria-selected=${isSelected ? 'true' : 'false'}\n                  class=${cn(\n                    'focus-visible:ring-ring inline-flex items-center gap-1.5 rounded-full px-3.5 py-1.5 text-xs transition-colors focus-visible:ring-2 focus-visible:outline-none',\n                    isSelected\n                      ? 'bg-primary text-primary-foreground font-medium shadow-xs'\n                      : 'bg-muted/70 text-muted-foreground hover:bg-muted hover:text-foreground',\n                  )}\n                  @click=${() => {\n                    this.archiveCategory = cat\n                  }}\n                >\n                  <span>${cat}</span>\n                  <span\n                    class=${cn(\n                      'py-0.2 rounded-full px-1.5 text-xs',\n                      isSelected\n                        ? 'bg-primary-foreground/20 text-primary-foreground'\n                        : 'bg-background/80 text-muted-foreground',\n                    )}\n                  >\n                    ${this.categoryCount(cat)}\n                  </span>\n                </button>\n              `\n            })}\n          </div>\n        </section>\n\n        <section aria-label=\"Archive issue list\">\n          ${filtered.length > 0\n            ? html`\n                <div class=\"grid grid-cols-1 gap-5 md:grid-cols-2\">\n                  ${filtered.map(\n                    (issue) => html`\n                      <uip-card\n                        class=\"group border-border/70 bg-card hover:border-primary/40 flex flex-col justify-between transition-colors hover:shadow-xs\"\n                      >\n                        <uip-card-header class=\"pb-3\">\n                          <div class=\"flex items-center justify-between gap-2\">\n                            <uip-badge variant=\"outline\" class=\"font-mono text-xs\">\n                              Issue #${issue.issueNumber} · ${issue.volumeDate}\n                            </uip-badge>\n                            <span class=\"text-muted-foreground inline-flex items-center gap-1 text-xs\">\n                              ${icon(Clock, 'clock', 'size-3')} ${issue.readTime}\n                            </span>\n                          </div>\n\n                          <uip-card-title\n                            class=\"group-hover:text-primary cursor-pointer pt-2 text-base leading-snug font-semibold tracking-tight transition-colors sm:text-lg\"\n                          >\n                            ${issue.title}\n                          </uip-card-title>\n\n                          <uip-card-description class=\"text-muted-foreground mt-1.5 line-clamp-3 text-sm leading-relaxed\">\n                            ${issue.excerpt}\n                          </uip-card-description>\n                        </uip-card-header>\n\n                        <uip-card-content class=\"py-0\">\n                          <div class=\"flex flex-wrap gap-1.5 pt-1\">\n                            ${issue.tags.map(\n                              (tag) => html`<uip-badge variant=\"secondary\" class=\"font-mono text-xs font-normal\">\n                                ${tag}\n                              </uip-badge>`,\n                            )}\n                          </div>\n                        </uip-card-content>\n\n                        <uip-card-footer\n                          class=\"border-border/60 text-muted-foreground mt-4 flex items-center justify-between border-t pt-4 text-xs\"\n                        >\n                          <div class=\"flex items-center gap-3\">\n                            <span class=\"inline-flex items-center gap-1\">\n                              ${icon(Calendar, 'calendar', 'size-3.5')} ${issue.date}\n                            </span>\n                            <span class=\"inline-flex items-center gap-1\">\n                              ${icon(MessageSquare, 'message-square', 'size-3.5')} ${issue.commentsCount}\n                            </span>\n                            <span class=\"inline-flex items-center gap-1\">\n                              ${icon(Heart, 'heart', 'size-3.5')} ${issue.likesCount}\n                            </span>\n                          </div>\n\n                          <a\n                            href=\"#read\"\n                            class=\"text-foreground group-hover:text-primary inline-flex items-center gap-1 font-medium transition-colors focus-visible:underline focus-visible:outline-none\"\n                          >\n                            <span>Read Issue</span>\n                            ${icon(ArrowRight, 'arrow-right', 'size-3.5 transition-transform group-hover:translate-x-0.5')}\n                          </a>\n                        </uip-card-footer>\n                      </uip-card>\n                    `,\n                  )}\n                </div>\n              `\n            : html`\n                <div class=\"border-border bg-card/50 space-y-3 rounded-xl border border-dashed p-10 text-center\">\n                  ${icon(Newspaper, 'newspaper', 'text-muted-foreground/60 mx-auto size-8')}\n                  <h3 class=\"text-foreground text-base font-semibold\">No issues found</h3>\n                  <p class=\"text-muted-foreground mx-auto max-w-sm text-sm\">\n                    No newsletter issues matched &ldquo;${this.archiveQuery}&rdquo; in ${this.archiveCategory}.\n                  </p>\n                  <div class=\"pt-2\">\n                    <uip-button variant=\"outline\" size=\"sm\" @click=${this.clearArchiveFilters}>\n                      Clear search &amp; filters\n                    </uip-button>\n                  </div>\n                </div>\n              `}\n        </section>\n\n        <footer\n          class=\"border-border/80 bg-muted/30 flex flex-col items-center justify-between gap-4 rounded-xl border p-6 text-center shadow-xs sm:flex-row sm:text-left\"\n        >\n          <div>\n            <h3 class=\"text-foreground text-sm font-semibold\">Never miss a teardown</h3>\n            <p class=\"text-muted-foreground mt-0.5 text-xs\">\n              Published every Tuesday morning. Zero sponsor promotions, purely deep technical architecture.\n            </p>\n          </div>\n\n          <div class=\"flex shrink-0 items-center gap-2.5\">\n            <uip-button variant=\"outline\" size=\"sm\" class=\"gap-1.5 text-xs\">\n              ${icon(Rss, 'rss', 'size-3.5')} RSS Feed\n            </uip-button>\n            <uip-button\n              variant=\"default\"\n              size=\"sm\"\n              class=\"gap-1.5 text-xs\"\n              @click=${() => {\n                const input = this.renderRoot.querySelector<HTMLElement>('#newsletter-archive-email-lit')\n                input?.focus()\n              }}\n            >\n              ${icon(Bookmark, 'bookmark', 'size-3.5')} Join Newsletter\n            </uip-button>\n          </div>\n        </footer>\n      </div>\n    `\n  }\n}\n\ncustomElements.get('uip-newsletter') || customElements.define('uip-newsletter', UipNewsletter)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-newsletter': UipNewsletter\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/blocks/newsletter/newsletter.ts"
    }
  ],
  "dependencies": [
    "lit",
    "lucide"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/badge.json",
    "https://uipkge.dev/r/lit/button.json",
    "https://uipkge.dev/r/lit/card.json",
    "https://uipkge.dev/r/lit/input.json",
    "https://uipkge.dev/r/lit/lit-core.json",
    "https://uipkge.dev/r/lit/separator.json"
  ]
}
