{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "utm-campaign-builder",
  "title": "Utm Campaign Builder",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-vue/blocks/utm-campaign-builder/UtmCampaignBuilder.vue",
      "content": "<script setup lang=\"ts\">\nimport type { HTMLAttributes } from 'vue'\nimport { computed, ref, watch } from 'vue'\nimport {\n  ArrowUpRight,\n  BarChart3,\n  Check,\n  CheckCircle2,\n  Copy,\n  Download,\n  ExternalLink,\n  Globe,\n  Hash,\n  Layers,\n  Link2,\n  QrCode,\n  RotateCcw,\n  Share2,\n  Tag,\n  Target,\n  TrendingUp,\n} from 'lucide-vue-next'\nimport { cn } from '@/lib/utils'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'\nimport { Input } from '@/components/ui/input'\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'\nimport { Separator } from '@/components/ui/separator'\nimport { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'\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\ninterface Props {\n  class?: HTMLAttributes['class']\n  initialPreset?: string\n  initialBaseUrl?: string\n  initialSource?: string\n  initialMedium?: string\n  initialCampaign?: string\n  initialTerm?: string\n  initialContent?: string\n  initialShortlinkDomain?: string\n  initialShortlinkSlug?: string\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  initialPreset: 'product_launch',\n  initialBaseUrl: 'https://uipkge.dev/pricing',\n  initialSource: 'twitter',\n  initialMedium: 'social',\n  initialCampaign: 'launch_2026_q3',\n  initialTerm: 'ui_components',\n  initialContent: 'banner_hero_cta',\n  initialShortlinkDomain: 'https://uipk.ge',\n  initialShortlinkSlug: 'q3-launch',\n})\n\nconst emits = defineEmits<{\n  (e: 'copy-url', url: string): void\n  (e: 'copy-shortlink', shortlink: string): void\n  (e: 'download-qr', payload: { url: string; filename: string }): void\n  (e: 'change', data: UtmCampaignData): void\n}>()\n\nconst 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\n// Form State\nconst selectedPreset = ref(props.initialPreset)\nconst baseUrl = ref(props.initialBaseUrl)\nconst utmSource = ref(props.initialSource)\nconst utmMedium = ref(props.initialMedium)\nconst utmCampaign = ref(props.initialCampaign)\nconst utmTerm = ref(props.initialTerm)\nconst utmContent = ref(props.initialContent)\n\nconst shortlinkDomain = ref(props.initialShortlinkDomain)\nconst shortlinkSlug = ref(props.initialShortlinkSlug)\n\n// Feedback states\nconst copiedFullUrl = ref(false)\nconst copiedShortlink = ref(false)\nconst downloadedQr = ref(false)\nlet copyUrlTimer: ReturnType<typeof setTimeout> | null = null\nlet copyShortlinkTimer: ReturnType<typeof setTimeout> | null = null\nlet downloadQrTimer: ReturnType<typeof setTimeout> | null = null\n\nfunction applyPreset(presetId: unknown) {\n  const id = String(presetId)\n  selectedPreset.value = id\n  if (id === 'custom') return\n\n  const preset = CAMPAIGN_PRESETS.find((p) => p.id === id)\n  if (preset) {\n    baseUrl.value = preset.baseUrl\n    utmSource.value = preset.source\n    utmMedium.value = preset.medium\n    utmCampaign.value = preset.campaign\n    utmTerm.value = preset.term ?? ''\n    utmContent.value = preset.content ?? ''\n    shortlinkSlug.value = preset.shortlinkSlug\n  }\n}\n\nfunction handleReset() {\n  selectedPreset.value = 'product_launch'\n  const def = CAMPAIGN_PRESETS[0]\n  baseUrl.value = def.baseUrl\n  utmSource.value = def.source\n  utmMedium.value = def.medium\n  utmCampaign.value = def.campaign\n  utmTerm.value = def.term ?? ''\n  utmContent.value = def.content ?? ''\n  shortlinkSlug.value = def.shortlinkSlug\n}\n\n// Generated parameters array for structured preview\ninterface QueryParamItem {\n  key: string\n  value: string\n  label: string\n  required: boolean\n}\n\nconst activeQueryParameters = computed<QueryParamItem[]>(() => {\n  const list: QueryParamItem[] = []\n  if (utmSource.value.trim()) {\n    list.push({\n      key: 'utm_source',\n      value: utmSource.value.trim(),\n      label: 'Campaign Source',\n      required: true,\n    })\n  }\n  if (utmMedium.value.trim()) {\n    list.push({\n      key: 'utm_medium',\n      value: utmMedium.value.trim(),\n      label: 'Campaign Medium',\n      required: true,\n    })\n  }\n  if (utmCampaign.value.trim()) {\n    list.push({\n      key: 'utm_campaign',\n      value: utmCampaign.value.trim(),\n      label: 'Campaign Name',\n      required: true,\n    })\n  }\n  if (utmTerm.value.trim()) {\n    list.push({\n      key: 'utm_term',\n      value: utmTerm.value.trim(),\n      label: 'Campaign Term',\n      required: false,\n    })\n  }\n  if (utmContent.value.trim()) {\n    list.push({\n      key: 'utm_content',\n      value: utmContent.value.trim(),\n      label: 'Campaign Content',\n      required: false,\n    })\n  }\n  return list\n})\n\n// Full Generated URL\nconst generatedFullUrl = computed(() => {\n  const rawBase = baseUrl.value.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 (utmSource.value.trim()) params.set('utm_source', utmSource.value.trim())\n  if (utmMedium.value.trim()) params.set('utm_medium', utmMedium.value.trim())\n  if (utmCampaign.value.trim()) params.set('utm_campaign', utmCampaign.value.trim())\n  if (utmTerm.value.trim()) params.set('utm_term', utmTerm.value.trim())\n  if (utmContent.value.trim()) params.set('utm_content', utmContent.value.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// Shortlink preview computation\nconst generatedShortlink = computed(() => {\n  const domain = shortlinkDomain.value.replace(/\\/+$/, '')\n  const slug =\n    shortlinkSlug.value.trim() ||\n    utmCampaign.value\n      .trim()\n      .toLowerCase()\n      .replace(/[^a-z0-9]+/g, '-') ||\n    'link'\n  return `${domain}/${slug}`\n})\n\n// Check if all 3 required UTM parameters are filled\nconst isGa4Compliant = computed(() => {\n  return Boolean(baseUrl.value.trim() && utmSource.value.trim() && utmMedium.value.trim() && utmCampaign.value.trim())\n})\n\n// Notify changes\nwatch(\n  [baseUrl, utmSource, utmMedium, utmCampaign, utmTerm, utmContent, generatedFullUrl, generatedShortlink],\n  () => {\n    emits('change', {\n      baseUrl: baseUrl.value,\n      source: utmSource.value,\n      medium: utmMedium.value,\n      campaign: utmCampaign.value,\n      term: utmTerm.value,\n      content: utmContent.value,\n      fullUrl: generatedFullUrl.value,\n      shortlinkUrl: generatedShortlink.value,\n    })\n  },\n  { immediate: true },\n)\n\n// Copy Handlers\nasync function handleCopyFullUrl() {\n  const url = generatedFullUrl.value\n  if (!url) return\n\n  try {\n    if (navigator.clipboard && window.isSecureContext) {\n      await navigator.clipboard.writeText(url)\n    } else {\n      const textarea = document.createElement('textarea')\n      textarea.value = url\n      textarea.style.position = 'fixed'\n      textarea.style.opacity = '0'\n      document.body.appendChild(textarea)\n      textarea.focus()\n      textarea.select()\n      document.execCommand('copy')\n      document.body.removeChild(textarea)\n    }\n\n    if (copyUrlTimer) clearTimeout(copyUrlTimer)\n    copiedFullUrl.value = true\n    emits('copy-url', url)\n    copyUrlTimer = setTimeout(() => {\n      copiedFullUrl.value = false\n    }, 2000)\n  } catch (err) {\n    console.error('Failed to copy URL:', err)\n  }\n}\n\nasync function handleCopyShortlink() {\n  const link = generatedShortlink.value\n  if (!link) return\n\n  try {\n    if (navigator.clipboard && window.isSecureContext) {\n      await navigator.clipboard.writeText(link)\n    } else {\n      const textarea = document.createElement('textarea')\n      textarea.value = link\n      textarea.style.position = 'fixed'\n      textarea.style.opacity = '0'\n      document.body.appendChild(textarea)\n      textarea.focus()\n      textarea.select()\n      document.execCommand('copy')\n      document.body.removeChild(textarea)\n    }\n\n    if (copyShortlinkTimer) clearTimeout(copyShortlinkTimer)\n    copiedShortlink.value = true\n    emits('copy-shortlink', link)\n    copyShortlinkTimer = setTimeout(() => {\n      copiedShortlink.value = false\n    }, 2000)\n  } catch (err) {\n    console.error('Failed to copy shortlink:', err)\n  }\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\nconst qrData = computed(() => {\n  const text = generatedFullUrl.value || 'https://uipkge.dev'\n  return generateQrMatrix(text)\n})\n\nconst qrPath = computed(() => qrToSvgPath(qrData.value.matrix))\n\nfunction handleDownloadQr() {\n  const text = generatedFullUrl.value || '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 = utmCampaign.value.trim() ? `${utmCampaign.value.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  if (downloadQrTimer) clearTimeout(downloadQrTimer)\n  downloadedQr.value = true\n  emits('download-qr', { url: text, filename })\n  downloadQrTimer = setTimeout(() => {\n    downloadedQr.value = false\n  }, 2000)\n}\n</script>\n\n<template>\n  <div data-slot=\"utm-campaign-builder\" :class=\"cn('mx-auto w-full max-w-7xl space-y-6', props.class)\">\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            <TrendingUp class=\"size-4\" aria-hidden=\"true\" />\n          </div>\n          <h1 class=\"text-foreground text-xl font-bold tracking-tight sm:text-2xl\">Campaign URL & UTM Builder</h1>\n          <Badge :variant=\"isGa4Compliant ? 'default' : 'outline'\" class=\"gap-1 px-2 py-0.5 text-xs font-semibold\">\n            <CheckCircle2 class=\"size-3\" aria-hidden=\"true\" />\n            {{ isGa4Compliant ? 'GA4 Standard' : 'Draft Link' }}\n          </Badge>\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          <Select :model-value=\"selectedPreset\" @update:model-value=\"applyPreset\">\n            <SelectTrigger size=\"sm\" class=\"h-9 text-xs\">\n              <SelectValue placeholder=\"Select campaign preset\" />\n            </SelectTrigger>\n            <SelectContent>\n              <SelectItem v-for=\"preset in CAMPAIGN_PRESETS\" :key=\"preset.id\" :value=\"preset.id\">\n                {{ preset.name }}\n              </SelectItem>\n              <SelectItem value=\"custom\">Custom Parameters</SelectItem>\n            </SelectContent>\n          </Select>\n        </div>\n\n        <Button variant=\"outline\" size=\"sm\" class=\"h-9 gap-1.5 text-xs\" @click=\"handleReset\">\n          <RotateCcw class=\"size-3.5\" aria-hidden=\"true\" />\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        <Card>\n          <CardHeader class=\"pb-4\">\n            <div class=\"flex items-center justify-between\">\n              <div class=\"flex items-center gap-2\">\n                <Link2 class=\"text-primary size-4\" aria-hidden=\"true\" />\n                <CardTitle class=\"text-base font-semibold\">Campaign Parameters</CardTitle>\n              </div>\n              <Badge variant=\"secondary\" class=\"font-mono text-xs\">\n                {{ activeQueryParameters.length }} active params\n              </Badge>\n            </div>\n            <CardDescription class=\"text-xs\">\n              Fill in the landing page destination and key attribution parameters for Google Analytics 4.\n            </CardDescription>\n          </CardHeader>\n\n          <CardContent 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              <Input\n                id=\"utm-base-url\"\n                v-model=\"baseUrl\"\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            <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\">utm_source</code>\n                </div>\n                <Badge variant=\"outline\" class=\"text-xs\">Required</Badge>\n              </div>\n              <Input id=\"utm-source\" v-model=\"utmSource\" placeholder=\"e.g. twitter, google, newsletter\" size=\"middle\" />\n              <!-- Quick Chips -->\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                <button\n                  v-for=\"chip in SOURCE_CHIPS\"\n                  :key=\"chip.label\"\n                  type=\"button\"\n                  :class=\"\n                    cn(\n                      'min-h-6 rounded-md border px-2 py-0.5 text-xs font-medium transition-colors',\n                      utmSource === chip.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                  @click=\"utmSource = chip.label\"\n                >\n                  {{ chip.label }}\n                </button>\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\">utm_medium</code>\n                </div>\n                <Badge variant=\"outline\" class=\"text-xs\">Required</Badge>\n              </div>\n              <Input\n                id=\"utm-medium\"\n                v-model=\"utmMedium\"\n                placeholder=\"e.g. social, cpc, email, referral\"\n                size=\"middle\"\n              />\n              <!-- Quick Chips -->\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                <button\n                  v-for=\"chip in MEDIUM_CHIPS\"\n                  :key=\"chip.label\"\n                  type=\"button\"\n                  :class=\"\n                    cn(\n                      'min-h-6 rounded-md border px-2 py-0.5 text-xs font-medium transition-colors',\n                      utmMedium === chip.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                  @click=\"utmMedium = chip.label\"\n                >\n                  {{ chip.label }}\n                </button>\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                <Badge variant=\"outline\" class=\"text-xs\">Required</Badge>\n              </div>\n              <Input\n                id=\"utm-campaign\"\n                v-model=\"utmCampaign\"\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            <Separator />\n\n            <!-- Optional Parameters Section Header -->\n            <div class=\"flex items-center gap-2 pt-1\">\n              <Tag class=\"text-muted-foreground size-3.5\" aria-hidden=\"true\" />\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              <Input id=\"utm-term\" v-model=\"utmTerm\" placeholder=\"e.g. ui_components, vue_dashboard\" size=\"middle\" />\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              <Input\n                id=\"utm-content\"\n                v-model=\"utmContent\"\n                placeholder=\"e.g. banner_cta_blue, sidebar_link_v2\"\n                size=\"middle\"\n              />\n            </div>\n          </CardContent>\n        </Card>\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        <Card class=\"border-border overflow-hidden shadow-sm\">\n          <CardHeader 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\" />\n                  <span class=\"bg-success relative inline-flex size-2 rounded-full\" />\n                </span>\n                <CardTitle class=\"text-base font-semibold\">Generated Campaign URL</CardTitle>\n              </div>\n              <Badge variant=\"outline\" class=\"border-success/30 bg-success/10 text-success text-xs font-medium\">\n                Live Ready\n              </Badge>\n            </div>\n            <CardDescription class=\"text-xs\">\n              Formatted destination URL with tracking query parameters.\n            </CardDescription>\n          </CardHeader>\n\n          <CardContent 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              <div v-if=\"generatedFullUrl\">\n                <span class=\"text-foreground font-semibold\">{{ baseUrl.trim() }}</span>\n                <template v-if=\"activeQueryParameters.length > 0\">\n                  <span class=\"text-muted-foreground font-bold\">{{ baseUrl.includes('?') ? '&' : '?' }}</span>\n                  <template v-for=\"(param, index) in activeQueryParameters\" :key=\"param.key\">\n                    <span class=\"text-info font-medium\">{{ param.key }}</span>\n                    <span class=\"text-muted-foreground\">=</span>\n                    <span class=\"text-success font-semibold\">{{ encodeURIComponent(param.value) }}</span>\n                    <span v-if=\"index < activeQueryParameters.length - 1\" class=\"text-muted-foreground font-bold\"\n                      >&amp;</span\n                    >\n                  </template>\n                </template>\n              </div>\n              <div v-else class=\"text-muted-foreground italic\">\n                Enter a destination website URL to generate your campaign link...\n              </div>\n            </div>\n\n            <!-- Action Buttons -->\n            <div class=\"grid grid-cols-1 gap-2 sm:grid-cols-2\">\n              <Button\n                size=\"sm\"\n                class=\"w-full gap-1.5 text-xs font-semibold\"\n                :disabled=\"!generatedFullUrl\"\n                @click=\"handleCopyFullUrl\"\n              >\n                <Check v-if=\"copiedFullUrl\" class=\"text-success size-3.5\" aria-hidden=\"true\" />\n                <Copy v-else class=\"size-3.5\" aria-hidden=\"true\" />\n                {{ copiedFullUrl ? 'Copied Full URL!' : 'Copy Full URL' }}\n              </Button>\n\n              <Button\n                variant=\"outline\"\n                size=\"sm\"\n                class=\"w-full gap-1.5 text-xs\"\n                :disabled=\"!generatedFullUrl\"\n                as=\"a\"\n                :href=\"generatedFullUrl || undefined\"\n                target=\"_blank\"\n                rel=\"noopener noreferrer\"\n              >\n                <ExternalLink class=\"size-3.5\" aria-hidden=\"true\" />\n                Test in New Tab\n              </Button>\n            </div>\n\n            <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                  <Globe class=\"text-primary size-3.5\" aria-hidden=\"true\" />\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                  <Input v-model=\"shortlinkSlug\" placeholder=\"custom-slug\" size=\"small\" class=\"font-mono text-xs\">\n                    <template #prefix>\n                      <span class=\"text-muted-foreground font-mono text-xs\">uipk.ge/</span>\n                    </template>\n                  </Input>\n                </div>\n                <Button variant=\"outline\" size=\"sm\" class=\"shrink-0 gap-1 text-xs\" @click=\"handleCopyShortlink\">\n                  <Check v-if=\"copiedShortlink\" class=\"text-success size-3\" aria-hidden=\"true\" />\n                  <Copy v-else class=\"size-3\" aria-hidden=\"true\" />\n                  {{ copiedShortlink ? 'Copied!' : 'Copy' }}\n                </Button>\n              </div>\n            </div>\n\n            <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                  <QrCode class=\"text-primary size-3.5\" aria-hidden=\"true\" />\n                  <span class=\"text-foreground text-xs font-semibold\">QR Code Campaign Pass</span>\n                </div>\n                <Badge variant=\"outline\" class=\"text-xs\">Print & Banners</Badge>\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                <!-- Crisp Vector SVG QR Code -->\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                    :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 :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                  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                  <Check v-if=\"downloadedQr\" class=\"text-success size-3.5\" aria-hidden=\"true\" />\n                  <Download v-else class=\"size-3.5\" aria-hidden=\"true\" />\n                  {{ downloadedQr ? 'Downloaded SVG!' : 'Download QR Code (.SVG)' }}\n                </Button>\n              </div>\n            </div>\n\n            <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>\n                  <TableHeader>\n                    <TableRow class=\"hover:bg-transparent\">\n                      <TableHead class=\"h-7 text-xs font-medium\">Parameter</TableHead>\n                      <TableHead class=\"h-7 text-xs font-medium\">Value</TableHead>\n                      <TableHead class=\"h-7 text-right text-xs font-medium\">Status</TableHead>\n                    </TableRow>\n                  </TableHeader>\n                  <TableBody>\n                    <TableRow>\n                      <TableCell class=\"py-2 font-mono text-xs font-semibold\">utm_source</TableCell>\n                      <TableCell class=\"text-muted-foreground py-2 font-mono text-xs\">\n                        {{ utmSource.trim() || '—' }}\n                      </TableCell>\n                      <TableCell class=\"py-2 text-right\">\n                        <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                        </Badge>\n                      </TableCell>\n                    </TableRow>\n\n                    <TableRow>\n                      <TableCell class=\"py-2 font-mono text-xs font-semibold\">utm_medium</TableCell>\n                      <TableCell class=\"text-muted-foreground py-2 font-mono text-xs\">\n                        {{ utmMedium.trim() || '—' }}\n                      </TableCell>\n                      <TableCell class=\"py-2 text-right\">\n                        <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                        </Badge>\n                      </TableCell>\n                    </TableRow>\n\n                    <TableRow>\n                      <TableCell class=\"py-2 font-mono text-xs font-semibold\">utm_campaign</TableCell>\n                      <TableCell class=\"text-muted-foreground py-2 font-mono text-xs\">\n                        {{ utmCampaign.trim() || '—' }}\n                      </TableCell>\n                      <TableCell class=\"py-2 text-right\">\n                        <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                        </Badge>\n                      </TableCell>\n                    </TableRow>\n\n                    <TableRow>\n                      <TableCell class=\"py-2 font-mono text-xs font-semibold\">utm_term</TableCell>\n                      <TableCell class=\"text-muted-foreground py-2 font-mono text-xs\">\n                        {{ utmTerm.trim() || '—' }}\n                      </TableCell>\n                      <TableCell class=\"py-2 text-right\">\n                        <Badge variant=\"outline\" class=\"px-1.5 py-0 text-xs font-normal\">\n                          {{ utmTerm.trim() ? 'Set' : 'Optional' }}\n                        </Badge>\n                      </TableCell>\n                    </TableRow>\n\n                    <TableRow>\n                      <TableCell class=\"py-2 font-mono text-xs font-semibold\">utm_content</TableCell>\n                      <TableCell class=\"text-muted-foreground py-2 font-mono text-xs\">\n                        {{ utmContent.trim() || '—' }}\n                      </TableCell>\n                      <TableCell class=\"py-2 text-right\">\n                        <Badge variant=\"outline\" class=\"px-1.5 py-0 text-xs font-normal\">\n                          {{ utmContent.trim() ? 'Set' : 'Optional' }}\n                        </Badge>\n                      </TableCell>\n                    </TableRow>\n                  </TableBody>\n                </Table>\n              </div>\n            </div>\n          </CardContent>\n        </Card>\n      </div>\n    </div>\n  </div>\n</template>\n",
      "type": "registry:block",
      "target": "~/app/components/blocks/UtmCampaignBuilder.vue"
    }
  ],
  "dependencies": [
    "lucide-vue-next"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/vue/badge.json",
    "https://uipkge.dev/r/vue/button.json",
    "https://uipkge.dev/r/vue/card.json",
    "https://uipkge.dev/r/vue/input.json",
    "https://uipkge.dev/r/vue/select.json",
    "https://uipkge.dev/r/vue/separator.json",
    "https://uipkge.dev/r/vue/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"
  ]
}