{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "utm-campaign-builder",
  "title": "Utm Campaign Builder",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-angular/blocks/utm-campaign-builder/utm-campaign-builder.component.ts",
      "content": "// Google Analytics 4 (GA4) campaign link generator with campaign presets, shortlink\n// generator, syntax-highlighted URL preview, parameter verification table, and vector\n// QR code export. Port of the React block 1:1.\nimport {\n  ChangeDetectionStrategy,\n  Component,\n  DestroyRef,\n  EventEmitter,\n  Input,\n  Output,\n  computed,\n  effect,\n  inject,\n  signal,\n} from '@angular/core'\nimport {\n  Check,\n  CheckCircle2,\n  Copy,\n  Download,\n  ExternalLink,\n  Globe,\n  Link2,\n  LucideAngularModule,\n  QrCode,\n  RotateCcw,\n  Tag,\n  TrendingUp,\n} from 'lucide-angular'\nimport { cn } from '@/lib/utils'\nimport { UiBadgeComponent } from '@/ui/badge/badge.component'\nimport { UiButtonComponent } from '@/ui/button/button.component'\nimport {\n  UiCardComponent,\n  UiCardContentComponent,\n  UiCardDescriptionComponent,\n  UiCardHeaderComponent,\n  UiCardTitleComponent,\n} from '@/ui/card/card.component'\nimport { UiInputComponent } from '@/ui/input/input.component'\nimport {\n  UiSelectComponent,\n  UiSelectContentComponent,\n  UiSelectItemComponent,\n  UiSelectTriggerComponent,\n  UiSelectValueComponent,\n} from '@/ui/select/select.component'\nimport { UiSeparatorComponent } from '@/ui/separator/separator.component'\nimport {\n  UiTableBodyComponent,\n  UiTableCellComponent,\n  UiTableComponent,\n  UiTableHeadComponent,\n  UiTableHeaderComponent,\n  UiTableRowComponent,\n} from '@/ui/table/table.component'\n\nexport interface UtmPreset {\n  id: string\n  name: string\n  description: string\n  baseUrl: string\n  source: string\n  medium: string\n  campaign: string\n  term?: string\n  content?: string\n  shortlinkSlug: string\n}\n\nexport interface UtmCampaignData {\n  baseUrl: string\n  source: string\n  medium: string\n  campaign: string\n  term: string\n  content: string\n  fullUrl: string\n  shortlinkUrl: string\n}\n\nexport const CAMPAIGN_PRESETS: UtmPreset[] = [\n  {\n    id: 'product_launch',\n    name: 'Product Launch',\n    description: 'SaaS feature launch across social platforms',\n    baseUrl: 'https://uipkge.dev/pricing',\n    source: 'twitter',\n    medium: 'social',\n    campaign: 'launch_2026_q3',\n    term: 'ui_components',\n    content: 'banner_hero_cta',\n    shortlinkSlug: 'q3-launch',\n  },\n  {\n    id: 'weekly_newsletter',\n    name: 'Weekly Newsletter',\n    description: 'Substack / Mailchimp subscriber digest campaign',\n    baseUrl: 'https://uipkge.dev/blog',\n    source: 'newsletter',\n    medium: 'email',\n    campaign: 'weekly_digest_issue_48',\n    term: 'developer_tools',\n    content: 'featured_article_button',\n    shortlinkSlug: 'digest-48',\n  },\n  {\n    id: 'google_search_ads',\n    name: 'Google Search Ads',\n    description: 'Targeted Google Search CPC keyword campaign',\n    baseUrl: 'https://uipkge.dev',\n    source: 'google',\n    medium: 'cpc',\n    campaign: 'search_brand_core_us',\n    term: 'vue_react_components',\n    content: 'headline_v2_price_test',\n    shortlinkSlug: 'google-ads',\n  },\n  {\n    id: 'twitter_promo',\n    name: 'Twitter / X Promo',\n    description: 'Promotional & organic posts on X/Twitter',\n    baseUrl: 'https://uipkge.dev/components',\n    source: 'twitter',\n    medium: 'social',\n    campaign: 'flash_discount_august',\n    term: 'lifetime_deal',\n    content: 'video_card_preview',\n    shortlinkSlug: 'x-promo',\n  },\n]\n\nconst SOURCE_CHIPS = [\n  { label: 'twitter', display: 'Twitter / X' },\n  { label: 'google', display: 'Google' },\n  { label: 'newsletter', display: 'Newsletter' },\n  { label: 'linkedin', display: 'LinkedIn' },\n  { label: 'facebook', display: 'Meta / FB' },\n  { label: 'youtube', display: 'YouTube' },\n]\n\nconst MEDIUM_CHIPS = [\n  { label: 'social', display: 'social' },\n  { label: 'cpc', display: 'cpc' },\n  { label: 'email', display: 'email' },\n  { label: 'referral', display: 'referral' },\n  { label: 'organic_social', display: 'organic_social' },\n  { label: 'display', display: 'display' },\n]\n\ninterface QueryParamItem {\n  key: string\n  value: string\n  label: string\n  required: boolean\n}\n\n// Standalone Pure QR Matrix Generator (Byte mode, standard GF(256) Reed-Solomon)\nfunction generateQrMatrix(text: string): { matrix: boolean[][]; size: number } {\n  const bytes: number[] = []\n  for (let i = 0; i < text.length; i++) {\n    const code = text.charCodeAt(i)\n    if (code < 0x80) bytes.push(code)\n    else if (code < 0x800) {\n      bytes.push(0xc0 | (code >> 6), 0x80 | (code & 0x3f))\n    } else {\n      bytes.push(0xe0 | (code >> 12), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f))\n    }\n  }\n\n  const versions = [\n    { version: 1, size: 21, dataCap: 16, ecWords: 10, blocks: 1, alignPos: [] as number[] },\n    { version: 2, size: 25, dataCap: 28, ecWords: 16, blocks: 1, alignPos: [6, 18] },\n    { version: 3, size: 29, dataCap: 44, ecWords: 26, blocks: 1, alignPos: [6, 22] },\n    { version: 4, size: 33, dataCap: 64, ecWords: 18, blocks: 2, alignPos: [6, 26] },\n    { version: 5, size: 37, dataCap: 86, ecWords: 24, blocks: 2, alignPos: [6, 30] },\n    { version: 6, size: 41, dataCap: 108, ecWords: 16, blocks: 4, alignPos: [6, 34] },\n    { version: 7, size: 45, dataCap: 124, ecWords: 18, blocks: 4, alignPos: [6, 22, 38] },\n    { version: 8, size: 49, dataCap: 154, ecWords: 22, blocks: 4, alignPos: [6, 24, 42] },\n    { version: 9, size: 53, dataCap: 182, ecWords: 22, blocks: 5, alignPos: [6, 26, 46] },\n    { version: 10, size: 57, dataCap: 216, ecWords: 26, blocks: 5, alignPos: [6, 28, 50] },\n  ]\n\n  let v = versions.find((spec) => bytes.length <= spec.dataCap - 3)\n  if (!v) v = versions[versions.length - 1]\n\n  const size = v.size\n  const matrix: (boolean | null)[][] = Array.from({ length: size }, () => Array(size).fill(null))\n  const isFunction: boolean[][] = Array.from({ length: size }, () => Array(size).fill(false))\n\n  function placeFinder(r: number, c: number) {\n    for (let dr = -1; dr <= 7; dr++) {\n      for (let dc = -1; dc <= 7; dc++) {\n        const nr = r + dr\n        const nc = c + dc\n        if (nr >= 0 && nr < size && nc >= 0 && nc < size) {\n          isFunction[nr][nc] = true\n          if (dr >= 0 && dr <= 6 && dc >= 0 && dc <= 6) {\n            const isBorder = dr === 0 || dr === 6 || dc === 0 || dc === 6\n            const isCenter = dr >= 2 && dr <= 4 && dc >= 2 && dc <= 4\n            matrix[nr][nc] = isBorder || isCenter\n          } else {\n            matrix[nr][nc] = false\n          }\n        }\n      }\n    }\n  }\n\n  placeFinder(0, 0)\n  placeFinder(0, size - 7)\n  placeFinder(size - 7, 0)\n\n  for (const ar of v.alignPos) {\n    for (const ac of v.alignPos) {\n      if ((ar === 6 && ac === 6) || (ar === 6 && ac === size - 7) || (ar === size - 7 && ac === 6)) {\n        continue\n      }\n      if (isFunction[ar][ac]) continue\n\n      for (let dr = -2; dr <= 2; dr++) {\n        for (let dc = -2; dc <= 2; dc++) {\n          const nr = ar + dr\n          const nc = ac + dc\n          isFunction[nr][nc] = true\n          const isBorder = Math.abs(dr) === 2 || Math.abs(dc) === 2\n          const isCenter = dr === 0 && dc === 0\n          matrix[nr][nc] = isBorder || isCenter\n        }\n      }\n    }\n  }\n\n  for (let i = 0; i < size; i++) {\n    if (!isFunction[6][i]) {\n      isFunction[6][i] = true\n      matrix[6][i] = i % 2 === 0\n    }\n    if (!isFunction[i][6]) {\n      isFunction[i][6] = true\n      matrix[i][6] = i % 2 === 0\n    }\n  }\n\n  isFunction[4 * v.version + 9][8] = true\n  matrix[4 * v.version + 9][8] = true\n\n  for (let i = 0; i < 9; i++) {\n    if (i !== 6) {\n      isFunction[8][i] = true\n      isFunction[i][8] = true\n    }\n  }\n  for (let i = 0; i < 8; i++) {\n    isFunction[8][size - 1 - i] = true\n    isFunction[size - 1 - i][8] = true\n  }\n\n  const bitStream: number[] = []\n  function pushBits(val: number, len: number) {\n    for (let i = len - 1; i >= 0; i--) {\n      bitStream.push((val >> i) & 1)\n    }\n  }\n\n  pushBits(0b0100, 4)\n  pushBits(bytes.length, 8)\n  for (const b of bytes) {\n    pushBits(b, 8)\n  }\n\n  const totalDataBits = v.dataCap * 8\n  const termLen = Math.min(4, totalDataBits - bitStream.length)\n  pushBits(0, termLen)\n\n  while (bitStream.length % 8 !== 0) {\n    bitStream.push(0)\n  }\n\n  const padBytes = [0xec, 0x11]\n  let padIdx = 0\n  while (bitStream.length < totalDataBits) {\n    pushBits(padBytes[padIdx % 2], 8)\n    padIdx++\n  }\n\n  const dataWords: number[] = []\n  for (let i = 0; i < bitStream.length; i += 8) {\n    let word = 0\n    for (let j = 0; j < 8; j++) {\n      word = (word << 1) | bitStream[i + j]\n    }\n    dataWords.push(word)\n  }\n\n  const EXP: number[] = new Array(512)\n  const LOG: number[] = new Array(256)\n  let cur = 1\n  for (let i = 0; i < 255; i++) {\n    EXP[i] = cur\n    EXP[i + 255] = cur\n    LOG[cur] = i\n    cur = (cur << 1) ^ (cur >= 128 ? 0x11d : 0)\n  }\n  function gfMul(a: number, b: number): number {\n    if (a === 0 || b === 0) return 0\n    return EXP[LOG[a] + LOG[b]]\n  }\n\n  function getGeneratorPoly(numEc: number): number[] {\n    let poly = [1]\n    for (let i = 0; i < numEc; i++) {\n      const next = new Array(poly.length + 1).fill(0)\n      for (let j = 0; j < poly.length; j++) {\n        next[j] ^= gfMul(poly[j], EXP[i])\n        next[j + 1] ^= poly[j]\n      }\n      poly = next\n    }\n    return poly\n  }\n\n  const ecWordsPerBlock = v.ecWords\n  const numBlocks = v.blocks\n  const blockSize = Math.floor(dataWords.length / numBlocks)\n  const rawBlocks: number[][] = []\n  const ecBlocks: number[][] = []\n  const genPoly = getGeneratorPoly(ecWordsPerBlock)\n\n  for (let b = 0; b < numBlocks; b++) {\n    const blockData = dataWords.slice(b * blockSize, (b + 1) * blockSize)\n    rawBlocks.push(blockData)\n\n    const rem = new Array(blockData.length + ecWordsPerBlock).fill(0)\n    for (let i = 0; i < blockData.length; i++) rem[i] = blockData[i]\n    for (let i = 0; i < blockData.length; i++) {\n      const factor = rem[i]\n      if (factor !== 0) {\n        for (let j = 0; j < genPoly.length; j++) {\n          rem[i + j] ^= gfMul(genPoly[j], factor)\n        }\n      }\n    }\n    ecBlocks.push(rem.slice(blockData.length))\n  }\n\n  const finalBits: number[] = []\n  for (let i = 0; i < blockSize; i++) {\n    for (let b = 0; b < numBlocks; b++) {\n      const val = rawBlocks[b][i] ?? 0\n      for (let bit = 7; bit >= 0; bit--) {\n        finalBits.push((val >> bit) & 1)\n      }\n    }\n  }\n  for (let i = 0; i < ecWordsPerBlock; i++) {\n    for (let b = 0; b < numBlocks; b++) {\n      const val = ecBlocks[b][i] ?? 0\n      for (let bit = 7; bit >= 0; bit--) {\n        finalBits.push((val >> bit) & 1)\n      }\n    }\n  }\n\n  let bitPointer = 0\n  let right = size - 1\n  let upward = true\n\n  while (right > 0) {\n    if (right === 6) right--\n    const colList = [right, right - 1]\n    const rowList = upward\n      ? Array.from({ length: size }, (_, i) => size - 1 - i)\n      : Array.from({ length: size }, (_, i) => i)\n\n    for (const r of rowList) {\n      for (const c of colList) {\n        if (!isFunction[r][c]) {\n          let bit = 0\n          if (bitPointer < finalBits.length) {\n            bit = finalBits[bitPointer++]\n          }\n          const mask = (r + c) % 2 === 0\n          matrix[r][c] = (bit === 1) !== mask\n        }\n      }\n    }\n    right -= 2\n    upward = !upward\n  }\n\n  const formatInfo = [1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0]\n  const tlCoords: [number, number][] = [\n    [8, 0],\n    [8, 1],\n    [8, 2],\n    [8, 3],\n    [8, 4],\n    [8, 5],\n    [8, 7],\n    [8, 8],\n    [7, 8],\n    [5, 8],\n    [4, 8],\n    [3, 8],\n    [2, 8],\n    [1, 8],\n    [0, 8],\n  ]\n  for (let i = 0; i < 15; i++) {\n    const [r, c] = tlCoords[i]\n    matrix[r][c] = formatInfo[i] === 1\n  }\n\n  for (let i = 0; i < 8; i++) {\n    matrix[8][size - 1 - i] = formatInfo[i] === 1\n  }\n  for (let i = 8; i < 15; i++) {\n    matrix[size - (15 - i)][8] = formatInfo[i] === 1\n  }\n\n  const cleanMatrix: boolean[][] = matrix.map((row) => row.map((cell) => cell === true))\n  return { matrix: cleanMatrix, size }\n}\n\nfunction qrToSvgPath(matrix: boolean[][]): string {\n  let path = ''\n  for (let r = 0; r < matrix.length; r++) {\n    for (let c = 0; c < matrix[r].length; c++) {\n      if (matrix[r][c]) {\n        path += `M${c},${r}h1v1h-1z `\n      }\n    }\n  }\n  return path\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-utm-campaign-builder, [ui-utm-campaign-builder]',\n  standalone: true,\n  // React renders the root <div> itself: the host stays out of layout and `class` goes to the root.\n  host: { '[attr.class]': '\"contents\"' },\n  imports: [\n    LucideAngularModule,\n    UiBadgeComponent,\n    UiButtonComponent,\n    UiCardComponent,\n    UiCardContentComponent,\n    UiCardDescriptionComponent,\n    UiCardHeaderComponent,\n    UiCardTitleComponent,\n    UiInputComponent,\n    UiSelectComponent,\n    UiSelectContentComponent,\n    UiSelectItemComponent,\n    UiSelectTriggerComponent,\n    UiSelectValueComponent,\n    UiSeparatorComponent,\n    UiTableBodyComponent,\n    UiTableCellComponent,\n    UiTableComponent,\n    UiTableHeadComponent,\n    UiTableHeaderComponent,\n    UiTableRowComponent,\n  ],\n  template: `\n    <div data-slot=\"utm-campaign-builder\" [class]=\"rootClass\">\n      <!-- Header Section -->\n      <div class=\"border-border flex flex-col gap-4 border-b pb-5 sm:flex-row sm:items-center sm:justify-between\">\n        <div class=\"space-y-1\">\n          <div class=\"flex flex-wrap items-center gap-2.5\">\n            <div\n              class=\"bg-primary/10 text-primary border-primary/20 flex size-8 items-center justify-center rounded-md border\"\n            >\n              <lucide-icon [img]=\"TrendingUp\" class=\"size-4\" />\n            </div>\n            <h1 class=\"text-foreground text-xl font-bold tracking-tight sm:text-2xl\">Campaign URL & UTM Builder</h1>\n            <span\n              ui-badge\n              [variant]=\"isGa4Compliant() ? 'default' : 'outline'\"\n              class=\"gap-1 px-2 py-0.5 text-xs font-semibold\"\n            >\n              <lucide-icon [img]=\"CheckCircle2\" class=\"size-3\" />\n              {{ isGa4Compliant() ? 'GA4 Standard' : 'Draft Link' }}\n            </span>\n          </div>\n          <p class=\"text-muted-foreground text-xs sm:text-sm\">\n            Generate trackable marketing links with standardized GA4 parameters, shortlinks, and vector QR codes.\n          </p>\n        </div>\n\n        <!-- Preset Selector & Reset Action -->\n        <div class=\"flex flex-wrap items-center gap-2\">\n          <div class=\"w-56\">\n            <ui-select [value]=\"selectedPreset()\" (valueChange)=\"applyPreset($event)\">\n              <button ui-select-trigger class=\"h-9 text-xs\">\n                <ui-select-value placeholder=\"Select campaign preset\" />\n              </button>\n              <ui-select-content>\n                @for (preset of presets; track preset.id) {\n                  <ui-select-item [value]=\"preset.id\">{{ preset.name }}</ui-select-item>\n                }\n                <ui-select-item value=\"custom\">Custom Parameters</ui-select-item>\n              </ui-select-content>\n            </ui-select>\n          </div>\n\n          <button ui-button variant=\"outline\" size=\"sm\" class=\"h-9 gap-1.5 text-xs\" (click)=\"handleReset()\">\n            <lucide-icon [img]=\"RotateCcw\" class=\"size-3.5\" />\n            Reset\n          </button>\n        </div>\n      </div>\n\n      <!-- 2-Column Builder Layout -->\n      <div class=\"grid grid-cols-1 items-start gap-8 lg:grid-cols-12\">\n        <!-- Left Column: Parameter Inputs Form -->\n        <div class=\"space-y-6 lg:col-span-7\">\n          <div ui-card>\n            <div ui-card-header class=\"pb-4\">\n              <div class=\"flex items-center justify-between\">\n                <div class=\"flex items-center gap-2\">\n                  <lucide-icon [img]=\"Link2\" class=\"text-primary size-4\" />\n                  <h2 ui-card-title class=\"text-base font-semibold\">Campaign Parameters</h2>\n                </div>\n                <span ui-badge variant=\"secondary\" class=\"font-mono text-xs\">\n                  {{ activeQueryParameters().length }} active params\n                </span>\n              </div>\n              <p ui-card-description class=\"text-xs\">\n                Fill in the landing page destination and key attribution parameters for Google Analytics 4.\n              </p>\n            </div>\n\n            <div ui-card-content class=\"space-y-5\">\n              <!-- 1. Base Website URL -->\n              <div class=\"space-y-1.5\">\n                <div class=\"flex items-center justify-between\">\n                  <label for=\"utm-base-url\" class=\"text-foreground text-xs font-medium\">\n                    Website URL <span class=\"text-destructive\">*</span>\n                  </label>\n                  <span class=\"text-muted-foreground text-xs\">Destination link</span>\n                </div>\n                <ui-input\n                  id=\"utm-base-url\"\n                  [value]=\"baseUrl()\"\n                  (valueChange)=\"baseUrl.set($event)\"\n                  type=\"url\"\n                  placeholder=\"https://uipkge.dev/pricing\"\n                  size=\"middle\"\n                />\n                <p class=\"text-muted-foreground text-xs\">The full landing page URL where users will arrive.</p>\n              </div>\n\n              <ui-separator />\n\n              <!-- 2. Campaign Source (utm_source) -->\n              <div class=\"space-y-2\">\n                <div class=\"flex items-center justify-between\">\n                  <div class=\"flex items-center gap-1.5\">\n                    <label for=\"utm-source\" class=\"text-foreground text-xs font-medium\">\n                      Campaign Source <span class=\"text-destructive\">*</span>\n                    </label>\n                    <code class=\"bg-muted text-muted-foreground rounded px-1.5 py-0.5 font-mono text-xs\"\n                      >utm_source</code\n                    >\n                  </div>\n                  <span ui-badge variant=\"outline\" class=\"text-xs\">Required</span>\n                </div>\n                <ui-input\n                  id=\"utm-source\"\n                  [value]=\"utmSource()\"\n                  (valueChange)=\"utmSource.set($event)\"\n                  placeholder=\"e.g. twitter, google, newsletter\"\n                  size=\"middle\"\n                />\n                <div class=\"flex flex-wrap items-center gap-1.5 pt-1\">\n                  <span class=\"text-muted-foreground mr-1 text-xs\">Suggestions:</span>\n                  @for (chip of sourceChips; track chip.label) {\n                    <button type=\"button\" [class]=\"sourceChipClass(chip.label)\" (click)=\"utmSource.set(chip.label)\">\n                      {{ chip.label }}\n                    </button>\n                  }\n                </div>\n              </div>\n\n              <!-- 3. Campaign Medium (utm_medium) -->\n              <div class=\"space-y-2\">\n                <div class=\"flex items-center justify-between\">\n                  <div class=\"flex items-center gap-1.5\">\n                    <label for=\"utm-medium\" class=\"text-foreground text-xs font-medium\">\n                      Campaign Medium <span class=\"text-destructive\">*</span>\n                    </label>\n                    <code class=\"bg-muted text-muted-foreground rounded px-1.5 py-0.5 font-mono text-xs\"\n                      >utm_medium</code\n                    >\n                  </div>\n                  <span ui-badge variant=\"outline\" class=\"text-xs\">Required</span>\n                </div>\n                <ui-input\n                  id=\"utm-medium\"\n                  [value]=\"utmMedium()\"\n                  (valueChange)=\"utmMedium.set($event)\"\n                  placeholder=\"e.g. social, cpc, email, referral\"\n                  size=\"middle\"\n                />\n                <div class=\"flex flex-wrap items-center gap-1.5 pt-1\">\n                  <span class=\"text-muted-foreground mr-1 text-xs\">Suggestions:</span>\n                  @for (chip of mediumChips; track chip.label) {\n                    <button type=\"button\" [class]=\"mediumChipClass(chip.label)\" (click)=\"utmMedium.set(chip.label)\">\n                      {{ chip.label }}\n                    </button>\n                  }\n                </div>\n              </div>\n\n              <!-- 4. Campaign Name (utm_campaign) -->\n              <div class=\"space-y-1.5\">\n                <div class=\"flex items-center justify-between\">\n                  <div class=\"flex items-center gap-1.5\">\n                    <label for=\"utm-campaign\" class=\"text-foreground text-xs font-medium\">\n                      Campaign Name <span class=\"text-destructive\">*</span>\n                    </label>\n                    <code class=\"bg-muted text-muted-foreground rounded px-1.5 py-0.5 font-mono text-xs\"\n                      >utm_campaign</code\n                    >\n                  </div>\n                  <span ui-badge variant=\"outline\" class=\"text-xs\">Required</span>\n                </div>\n                <ui-input\n                  id=\"utm-campaign\"\n                  [value]=\"utmCampaign()\"\n                  (valueChange)=\"utmCampaign.set($event)\"\n                  placeholder=\"e.g. launch_2026_q3, summer_promo\"\n                  size=\"middle\"\n                />\n                <p class=\"text-muted-foreground text-xs\">Unique promotion identifier, product slogan, or promo code.</p>\n              </div>\n\n              <ui-separator />\n\n              <!-- Optional Parameters Section Header -->\n              <div class=\"flex items-center gap-2 pt-1\">\n                <lucide-icon [img]=\"Tag\" class=\"text-muted-foreground size-3.5\" />\n                <span class=\"text-muted-foreground text-xs font-semibold tracking-wider uppercase\">\n                  Optional GA4 Attributes\n                </span>\n              </div>\n\n              <!-- 5. Campaign Term (utm_term) -->\n              <div class=\"space-y-1.5\">\n                <div class=\"flex items-center justify-between\">\n                  <div class=\"flex items-center gap-1.5\">\n                    <label for=\"utm-term\" class=\"text-foreground text-xs font-medium\">Campaign Term</label>\n                    <code class=\"bg-muted text-muted-foreground rounded px-1.5 py-0.5 font-mono text-xs\">utm_term</code>\n                  </div>\n                  <span class=\"text-muted-foreground text-xs\">Optional · Paid search keywords</span>\n                </div>\n                <ui-input\n                  id=\"utm-term\"\n                  [value]=\"utmTerm()\"\n                  (valueChange)=\"utmTerm.set($event)\"\n                  placeholder=\"e.g. ui_components, vue_dashboard\"\n                  size=\"middle\"\n                />\n              </div>\n\n              <!-- 6. Campaign Content (utm_content) -->\n              <div class=\"space-y-1.5\">\n                <div class=\"flex items-center justify-between\">\n                  <div class=\"flex items-center gap-1.5\">\n                    <label for=\"utm-content\" class=\"text-foreground text-xs font-medium\">Campaign Content</label>\n                    <code class=\"bg-muted text-muted-foreground rounded px-1.5 py-0.5 font-mono text-xs\"\n                      >utm_content</code\n                    >\n                  </div>\n                  <span class=\"text-muted-foreground text-xs\">Optional · A/B test & ad creative</span>\n                </div>\n                <ui-input\n                  id=\"utm-content\"\n                  [value]=\"utmContent()\"\n                  (valueChange)=\"utmContent.set($event)\"\n                  placeholder=\"e.g. banner_cta_blue, sidebar_link_v2\"\n                  size=\"middle\"\n                />\n              </div>\n            </div>\n          </div>\n        </div>\n\n        <!-- Right Column: Generated Campaign URL Card (Sticky) -->\n        <div class=\"space-y-5 lg:sticky lg:top-6 lg:col-span-5\">\n          <div ui-card class=\"border-border overflow-hidden shadow-sm\">\n            <div ui-card-header class=\"pb-3\">\n              <div class=\"flex items-center justify-between\">\n                <div class=\"flex items-center gap-2\">\n                  <span class=\"relative flex size-2\">\n                    <span class=\"bg-success absolute inline-flex h-full w-full rounded-full opacity-75\"></span>\n                    <span class=\"bg-success relative inline-flex size-2 rounded-full\"></span>\n                  </span>\n                  <h2 ui-card-title class=\"text-base font-semibold\">Generated Campaign URL</h2>\n                </div>\n                <span\n                  ui-badge\n                  variant=\"outline\"\n                  class=\"border-success/30 bg-success/10 text-success text-xs font-medium\"\n                >\n                  Live Ready\n                </span>\n              </div>\n              <p ui-card-description class=\"text-xs\">Formatted destination URL with tracking query parameters.</p>\n            </div>\n\n            <div ui-card-content class=\"space-y-4\">\n              <!-- Monospace Syntax-Highlighted Code Container -->\n              <div\n                class=\"bg-muted/70 border-border/80 relative rounded-lg border p-3.5 font-mono text-xs leading-relaxed break-all select-all\"\n              >\n                @if (generatedFullUrl()) {\n                  <div>\n                    <span class=\"text-foreground font-semibold\">{{ baseUrl().trim() }}</span>\n                    @if (activeQueryParameters().length > 0) {\n                      <span class=\"text-muted-foreground font-bold\">{{ baseUrl().includes('?') ? '&' : '?' }}</span>\n                      @for (param of activeQueryParameters(); track param.key; let last = $last) {\n                        <span class=\"text-info font-medium\">{{ param.key }}</span>\n                        <span class=\"text-muted-foreground\">=</span>\n                        <span class=\"text-success font-semibold\">{{ encodeParam(param.value) }}</span>\n                        @if (!last) {\n                          <span class=\"text-muted-foreground font-bold\">&amp;</span>\n                        }\n                      }\n                    }\n                  </div>\n                } @else {\n                  <div class=\"text-muted-foreground italic\">\n                    Enter a destination website URL to generate your campaign link...\n                  </div>\n                }\n              </div>\n\n              <!-- Action Buttons -->\n              <div class=\"grid grid-cols-1 gap-2 sm:grid-cols-2\">\n                <button\n                  ui-button\n                  size=\"sm\"\n                  class=\"w-full gap-1.5 text-xs font-semibold\"\n                  [disabled]=\"!generatedFullUrl()\"\n                  (click)=\"handleCopyFullUrl()\"\n                >\n                  @if (copiedFullUrl()) {\n                    <lucide-icon [img]=\"Check\" class=\"text-success size-3.5\" />\n                  } @else {\n                    <lucide-icon [img]=\"Copy\" class=\"size-3.5\" />\n                  }\n                  {{ copiedFullUrl() ? 'Copied Full URL!' : 'Copy Full URL' }}\n                </button>\n\n                <a\n                  ui-button\n                  variant=\"outline\"\n                  size=\"sm\"\n                  class=\"w-full gap-1.5 text-xs\"\n                  [attr.href]=\"generatedFullUrl() || null\"\n                  target=\"_blank\"\n                  rel=\"noopener noreferrer\"\n                >\n                  <lucide-icon [img]=\"ExternalLink\" class=\"size-3.5\" />\n                  Test in New Tab\n                </a>\n              </div>\n\n              <ui-separator />\n\n              <!-- Shortlink Generator Preview -->\n              <div class=\"space-y-2\">\n                <div class=\"flex items-center justify-between\">\n                  <div class=\"flex items-center gap-1.5\">\n                    <lucide-icon [img]=\"Globe\" class=\"text-primary size-3.5\" />\n                    <span class=\"text-foreground text-xs font-semibold\">Shortlink Generator</span>\n                  </div>\n                  <span class=\"text-muted-foreground text-xs\">For SMS & Social</span>\n                </div>\n\n                <div class=\"flex items-center gap-2\">\n                  <div class=\"relative flex-1\">\n                    <ui-input\n                      [value]=\"shortlinkSlug()\"\n                      (valueChange)=\"shortlinkSlug.set($event)\"\n                      placeholder=\"custom-slug\"\n                      size=\"small\"\n                      [prefix]=\"'uipk.ge/'\"\n                      class=\"font-mono text-xs\"\n                    />\n                  </div>\n                  <button\n                    ui-button\n                    variant=\"outline\"\n                    size=\"sm\"\n                    class=\"shrink-0 gap-1 text-xs\"\n                    (click)=\"handleCopyShortlink()\"\n                  >\n                    @if (copiedShortlink()) {\n                      <lucide-icon [img]=\"Check\" class=\"text-success size-3\" />\n                    } @else {\n                      <lucide-icon [img]=\"Copy\" class=\"size-3\" />\n                    }\n                    {{ copiedShortlink() ? 'Copied!' : 'Copy' }}\n                  </button>\n                </div>\n              </div>\n\n              <ui-separator />\n\n              <!-- SVG QR Code Generator Pass -->\n              <div class=\"space-y-3\">\n                <div class=\"flex items-center justify-between\">\n                  <div class=\"flex items-center gap-1.5\">\n                    <lucide-icon [img]=\"QrCode\" class=\"text-primary size-3.5\" />\n                    <span class=\"text-foreground text-xs font-semibold\">QR Code Campaign Pass</span>\n                  </div>\n                  <span ui-badge variant=\"outline\" class=\"text-xs\">Print & Banners</span>\n                </div>\n\n                <div\n                  class=\"bg-muted/40 border-border/70 flex flex-col items-center justify-center gap-3 rounded-lg border p-4 text-center\"\n                >\n                  <div\n                    class=\"bg-card text-foreground border-border flex size-36 items-center justify-center rounded-md border p-2 shadow-xs\"\n                  >\n                    <svg\n                      xmlns=\"http://www.w3.org/2000/svg\"\n                      [attr.viewBox]=\"'0 0 ' + (qrData().size + 4) + ' ' + (qrData().size + 4)\"\n                      class=\"size-full\"\n                      shape-rendering=\"crispEdges\"\n                    >\n                      <rect width=\"100%\" height=\"100%\" fill=\"transparent\" />\n                      <g transform=\"translate(2, 2)\">\n                        <path [attr.d]=\"qrPath()\" fill=\"currentColor\" />\n                      </g>\n                    </svg>\n                  </div>\n\n                  <p class=\"text-muted-foreground text-xs\">\n                    Vector SVG scales to any resolution for billboard, print collateral, or event badges.\n                  </p>\n\n                  <button\n                    ui-button\n                    aria-label=\"Download attachment\"\n                    variant=\"outline\"\n                    size=\"sm\"\n                    class=\"w-full gap-1.5 text-xs\"\n                    [disabled]=\"!generatedFullUrl()\"\n                    (click)=\"handleDownloadQr()\"\n                  >\n                    @if (downloadedQr()) {\n                      <lucide-icon [img]=\"Check\" class=\"text-success size-3.5\" />\n                    } @else {\n                      <lucide-icon [img]=\"Download\" class=\"size-3.5\" />\n                    }\n                    {{ downloadedQr() ? 'Downloaded SVG!' : 'Download QR Code (.SVG)' }}\n                  </button>\n                </div>\n              </div>\n\n              <ui-separator />\n\n              <!-- Parameters Breakdown Table -->\n              <div class=\"space-y-2\">\n                <div class=\"flex items-center justify-between\">\n                  <span class=\"text-foreground text-xs font-semibold\">Parameters Verification</span>\n                  <span class=\"text-muted-foreground text-xs\">{{ activeQueryParameters().length }}/5 Active</span>\n                </div>\n\n                <div class=\"border-border overflow-hidden rounded-md border text-xs\">\n                  <table ui-table>\n                    <thead ui-table-header>\n                      <tr ui-table-row class=\"hover:bg-transparent\">\n                        <th ui-table-head class=\"h-7 text-xs font-medium\">Parameter</th>\n                        <th ui-table-head class=\"h-7 text-xs font-medium\">Value</th>\n                        <th ui-table-head class=\"h-7 text-right text-xs font-medium\">Status</th>\n                      </tr>\n                    </thead>\n                    <tbody ui-table-body>\n                      <tr ui-table-row>\n                        <td ui-table-cell class=\"py-2 font-mono text-xs font-semibold\">utm_source</td>\n                        <td ui-table-cell class=\"text-muted-foreground py-2 font-mono text-xs\">\n                          {{ utmSource().trim() || '—' }}\n                        </td>\n                        <td ui-table-cell class=\"py-2 text-right\">\n                          <span\n                            ui-badge\n                            [variant]=\"utmSource().trim() ? 'default' : 'destructive'\"\n                            class=\"px-1.5 py-0 text-xs font-normal\"\n                          >\n                            {{ utmSource().trim() ? 'Set' : 'Missing' }}\n                          </span>\n                        </td>\n                      </tr>\n\n                      <tr ui-table-row>\n                        <td ui-table-cell class=\"py-2 font-mono text-xs font-semibold\">utm_medium</td>\n                        <td ui-table-cell class=\"text-muted-foreground py-2 font-mono text-xs\">\n                          {{ utmMedium().trim() || '—' }}\n                        </td>\n                        <td ui-table-cell class=\"py-2 text-right\">\n                          <span\n                            ui-badge\n                            [variant]=\"utmMedium().trim() ? 'default' : 'destructive'\"\n                            class=\"px-1.5 py-0 text-xs font-normal\"\n                          >\n                            {{ utmMedium().trim() ? 'Set' : 'Missing' }}\n                          </span>\n                        </td>\n                      </tr>\n\n                      <tr ui-table-row>\n                        <td ui-table-cell class=\"py-2 font-mono text-xs font-semibold\">utm_campaign</td>\n                        <td ui-table-cell class=\"text-muted-foreground py-2 font-mono text-xs\">\n                          {{ utmCampaign().trim() || '—' }}\n                        </td>\n                        <td ui-table-cell class=\"py-2 text-right\">\n                          <span\n                            ui-badge\n                            [variant]=\"utmCampaign().trim() ? 'default' : 'destructive'\"\n                            class=\"px-1.5 py-0 text-xs font-normal\"\n                          >\n                            {{ utmCampaign().trim() ? 'Set' : 'Missing' }}\n                          </span>\n                        </td>\n                      </tr>\n\n                      <tr ui-table-row>\n                        <td ui-table-cell class=\"py-2 font-mono text-xs font-semibold\">utm_term</td>\n                        <td ui-table-cell class=\"text-muted-foreground py-2 font-mono text-xs\">\n                          {{ utmTerm().trim() || '—' }}\n                        </td>\n                        <td ui-table-cell class=\"py-2 text-right\">\n                          <span ui-badge variant=\"outline\" class=\"px-1.5 py-0 text-xs font-normal\">\n                            {{ utmTerm().trim() ? 'Set' : 'Optional' }}\n                          </span>\n                        </td>\n                      </tr>\n\n                      <tr ui-table-row>\n                        <td ui-table-cell class=\"py-2 font-mono text-xs font-semibold\">utm_content</td>\n                        <td ui-table-cell class=\"text-muted-foreground py-2 font-mono text-xs\">\n                          {{ utmContent().trim() || '—' }}\n                        </td>\n                        <td ui-table-cell class=\"py-2 text-right\">\n                          <span ui-badge variant=\"outline\" class=\"px-1.5 py-0 text-xs font-normal\">\n                            {{ utmContent().trim() ? 'Set' : 'Optional' }}\n                          </span>\n                        </td>\n                      </tr>\n                    </tbody>\n                  </table>\n                </div>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  `,\n})\nexport class UiUtmCampaignBuilderComponent {\n  protected readonly Check = Check\n  protected readonly CheckCircle2 = CheckCircle2\n  protected readonly Copy = Copy\n  protected readonly Download = Download\n  protected readonly ExternalLink = ExternalLink\n  protected readonly Globe = Globe\n  protected readonly Link2 = Link2\n  protected readonly QrCode = QrCode\n  protected readonly RotateCcw = RotateCcw\n  protected readonly Tag = Tag\n  protected readonly TrendingUp = TrendingUp\n\n  protected readonly presets = CAMPAIGN_PRESETS\n  protected readonly sourceChips = SOURCE_CHIPS\n  protected readonly mediumChips = MEDIUM_CHIPS\n\n  @Input() set initialPreset(v: string) {\n    this.selectedPreset.set(v ?? 'product_launch')\n  }\n  @Input() set initialBaseUrl(v: string) {\n    if (v !== undefined) this.baseUrl.set(v)\n  }\n  @Input() set initialSource(v: string) {\n    if (v !== undefined) this.utmSource.set(v)\n  }\n  @Input() set initialMedium(v: string) {\n    if (v !== undefined) this.utmMedium.set(v)\n  }\n  @Input() set initialCampaign(v: string) {\n    if (v !== undefined) this.utmCampaign.set(v)\n  }\n  @Input() set initialTerm(v: string) {\n    if (v !== undefined) this.utmTerm.set(v)\n  }\n  @Input() set initialContent(v: string) {\n    if (v !== undefined) this.utmContent.set(v)\n  }\n  @Input() set initialShortlinkDomain(v: string) {\n    if (v !== undefined) this.shortlinkDomain.set(v)\n  }\n  @Input() set initialShortlinkSlug(v: string) {\n    if (v !== undefined) this.shortlinkSlug.set(v)\n  }\n  @Input('class') className?: string\n\n  // React onCopyUrl/onCopyShortlink/onDownloadQr/onChange (Vue copy-url/copy-shortlink/download-qr/change).\n  @Output() readonly copyUrl = new EventEmitter<string>()\n  @Output() readonly copyShortlink = new EventEmitter<string>()\n  @Output() readonly downloadQr = new EventEmitter<{ url: string; filename: string }>()\n  @Output() readonly change = new EventEmitter<UtmCampaignData>()\n\n  readonly selectedPreset = signal('product_launch')\n  readonly baseUrl = signal('https://uipkge.dev/pricing')\n  readonly utmSource = signal('twitter')\n  readonly utmMedium = signal('social')\n  readonly utmCampaign = signal('launch_2026_q3')\n  readonly utmTerm = signal('ui_components')\n  readonly utmContent = signal('banner_hero_cta')\n\n  readonly shortlinkDomain = signal('https://uipk.ge')\n  readonly shortlinkSlug = signal('q3-launch')\n\n  readonly copiedFullUrl = signal(false)\n  readonly copiedShortlink = signal(false)\n  readonly downloadedQr = signal(false)\n\n  private copyUrlTimer: ReturnType<typeof setTimeout> | undefined\n  private copyShortlinkTimer: ReturnType<typeof setTimeout> | undefined\n  private downloadQrTimer: ReturnType<typeof setTimeout> | undefined\n\n  constructor() {\n    inject(DestroyRef).onDestroy(() => {\n      clearTimeout(this.copyUrlTimer)\n      clearTimeout(this.copyShortlinkTimer)\n      clearTimeout(this.downloadQrTimer)\n    })\n    // React's onChange useEffect / Vue's immediate change watcher.\n    effect(() => {\n      this.change.emit({\n        baseUrl: this.baseUrl(),\n        source: this.utmSource(),\n        medium: this.utmMedium(),\n        campaign: this.utmCampaign(),\n        term: this.utmTerm(),\n        content: this.utmContent(),\n        fullUrl: this.generatedFullUrl(),\n        shortlinkUrl: this.generatedShortlink(),\n      })\n    })\n  }\n\n  readonly activeQueryParameters = computed<QueryParamItem[]>(() => {\n    const list: QueryParamItem[] = []\n    if (this.utmSource().trim()) {\n      list.push({ key: 'utm_source', value: this.utmSource().trim(), label: 'Campaign Source', required: true })\n    }\n    if (this.utmMedium().trim()) {\n      list.push({ key: 'utm_medium', value: this.utmMedium().trim(), label: 'Campaign Medium', required: true })\n    }\n    if (this.utmCampaign().trim()) {\n      list.push({ key: 'utm_campaign', value: this.utmCampaign().trim(), label: 'Campaign Name', required: true })\n    }\n    if (this.utmTerm().trim()) {\n      list.push({ key: 'utm_term', value: this.utmTerm().trim(), label: 'Campaign Term', required: false })\n    }\n    if (this.utmContent().trim()) {\n      list.push({ key: 'utm_content', value: this.utmContent().trim(), label: 'Campaign Content', required: false })\n    }\n    return list\n  })\n\n  readonly generatedFullUrl = computed(() => {\n    const rawBase = this.baseUrl().trim()\n    if (!rawBase) return ''\n\n    let normalized = rawBase\n    if (!/^https?:\\/\\//i.test(normalized)) {\n      normalized = `https://${normalized}`\n    }\n\n    const params = new URLSearchParams()\n    if (this.utmSource().trim()) params.set('utm_source', this.utmSource().trim())\n    if (this.utmMedium().trim()) params.set('utm_medium', this.utmMedium().trim())\n    if (this.utmCampaign().trim()) params.set('utm_campaign', this.utmCampaign().trim())\n    if (this.utmTerm().trim()) params.set('utm_term', this.utmTerm().trim())\n    if (this.utmContent().trim()) params.set('utm_content', this.utmContent().trim())\n\n    const queryString = params.toString()\n    if (!queryString) return normalized\n\n    const separator = normalized.includes('?') ? '&' : '?'\n    return `${normalized}${separator}${queryString}`\n  })\n\n  readonly generatedShortlink = computed(() => {\n    const domain = this.shortlinkDomain().replace(/\\/+$/, '')\n    const slug =\n      this.shortlinkSlug().trim() ||\n      this.utmCampaign()\n        .trim()\n        .toLowerCase()\n        .replace(/[^a-z0-9]+/g, '-') ||\n      'link'\n    return `${domain}/${slug}`\n  })\n\n  readonly isGa4Compliant = computed(() =>\n    Boolean(this.baseUrl().trim() && this.utmSource().trim() && this.utmMedium().trim() && this.utmCampaign().trim()),\n  )\n\n  readonly qrData = computed(() => {\n    const text = this.generatedFullUrl() || 'https://uipkge.dev'\n    return generateQrMatrix(text)\n  })\n\n  readonly qrPath = computed(() => qrToSvgPath(this.qrData().matrix))\n\n  get rootClass(): string {\n    return cn('mx-auto w-full max-w-7xl space-y-6', this.className)\n  }\n\n  encodeParam(value: string): string {\n    return encodeURIComponent(value)\n  }\n\n  sourceChipClass(label: string): string {\n    return cn(\n      'min-h-6 rounded-md border px-2 py-0.5 text-xs font-medium transition-colors',\n      this.utmSource() === label\n        ? 'border-primary bg-primary/10 text-primary font-semibold'\n        : 'border-border bg-muted/40 text-muted-foreground hover:bg-muted hover:text-foreground',\n    )\n  }\n\n  mediumChipClass(label: string): string {\n    return cn(\n      'min-h-6 rounded-md border px-2 py-0.5 text-xs font-medium transition-colors',\n      this.utmMedium() === label\n        ? 'border-primary bg-primary/10 text-primary font-semibold'\n        : 'border-border bg-muted/40 text-muted-foreground hover:bg-muted hover:text-foreground',\n    )\n  }\n\n  applyPreset(presetId: string): void {\n    const id = String(presetId)\n    this.selectedPreset.set(id)\n    if (id === 'custom') return\n\n    const preset = CAMPAIGN_PRESETS.find((p) => p.id === id)\n    if (preset) {\n      this.baseUrl.set(preset.baseUrl)\n      this.utmSource.set(preset.source)\n      this.utmMedium.set(preset.medium)\n      this.utmCampaign.set(preset.campaign)\n      this.utmTerm.set(preset.term ?? '')\n      this.utmContent.set(preset.content ?? '')\n      this.shortlinkSlug.set(preset.shortlinkSlug)\n    }\n  }\n\n  handleReset(): void {\n    this.selectedPreset.set('product_launch')\n    const def = CAMPAIGN_PRESETS[0]\n    this.baseUrl.set(def.baseUrl)\n    this.utmSource.set(def.source)\n    this.utmMedium.set(def.medium)\n    this.utmCampaign.set(def.campaign)\n    this.utmTerm.set(def.term ?? '')\n    this.utmContent.set(def.content ?? '')\n    this.shortlinkSlug.set(def.shortlinkSlug)\n  }\n\n  async handleCopyFullUrl(): Promise<void> {\n    const url = this.generatedFullUrl()\n    if (!url) return\n    try {\n      await navigator.clipboard.writeText(url)\n      clearTimeout(this.copyUrlTimer)\n      this.copiedFullUrl.set(true)\n      this.copyUrl.emit(url)\n      this.copyUrlTimer = setTimeout(() => this.copiedFullUrl.set(false), 2000)\n    } catch {\n      // Clipboard unavailable (insecure context): leave the button state unchanged.\n    }\n  }\n\n  async handleCopyShortlink(): Promise<void> {\n    const link = this.generatedShortlink()\n    if (!link) return\n    try {\n      await navigator.clipboard.writeText(link)\n      clearTimeout(this.copyShortlinkTimer)\n      this.copiedShortlink.set(true)\n      this.copyShortlink.emit(link)\n      this.copyShortlinkTimer = setTimeout(() => this.copiedShortlink.set(false), 2000)\n    } catch {\n      // Clipboard unavailable (insecure context): leave the button state unchanged.\n    }\n  }\n\n  handleDownloadQr(): void {\n    const text = this.generatedFullUrl() || 'https://uipkge.dev'\n    const { matrix, size } = generateQrMatrix(text)\n    const pathData = qrToSvgPath(matrix)\n    const margin = 2\n    const totalSize = size + margin * 2\n    const filename = this.utmCampaign().trim() ? `${this.utmCampaign().trim()}-qr.svg` : 'utm-campaign-qr.svg'\n\n    const svgContent = `<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 ${totalSize} ${totalSize}\" width=\"512\" height=\"512\">\n  <rect width=\"100%\" height=\"100%\" fill=\"#ffffff\" />\n  <g transform=\"translate(${margin}, ${margin})\">\n    <path d=\"${pathData}\" fill=\"#0f172a\" shape-rendering=\"crispEdges\" />\n  </g>\n</svg>`\n\n    const blob = new Blob([svgContent], { type: 'image/svg+xml;charset=utf-8' })\n    const url = URL.createObjectURL(blob)\n    const link = document.createElement('a')\n    link.href = url\n    link.download = filename\n    document.body.appendChild(link)\n    link.click()\n    document.body.removeChild(link)\n    URL.revokeObjectURL(url)\n\n    clearTimeout(this.downloadQrTimer)\n    this.downloadedQr.set(true)\n    this.downloadQr.emit({ url: text, filename })\n    this.downloadQrTimer = setTimeout(() => this.downloadedQr.set(false), 2000)\n  }\n}\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/utm-campaign-builder/utm-campaign-builder.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/utm-campaign-builder/index.ts",
      "content": "export {\n  UiUtmCampaignBuilderComponent,\n  CAMPAIGN_PRESETS,\n  type UtmPreset,\n  type UtmCampaignData,\n} from './utm-campaign-builder.component'\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/utm-campaign-builder/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/card.json",
    "https://uipkge.dev/r/angular/input.json",
    "https://uipkge.dev/r/angular/select.json",
    "https://uipkge.dev/r/angular/separator.json",
    "https://uipkge.dev/r/angular/table.json"
  ],
  "description": "Google Analytics 4 (GA4) campaign link generator with preset presets, shortlink generator, syntax-highlighted URL preview, parameter verification table, and vector QR code export.",
  "categories": [
    "marketing",
    "app",
    "form"
  ]
}