{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "article-50-badge",
  "type": "registry:component",
  "title": "EU AI Act Article 50 AI Notice Badge",
  "description": "Visible EU AI Act Art. 50 AI notice in all 24 EU languages. Loads https://spurvik.com/badge.js (MIT, zero cookies, no telemetry, Shadow DOM). attribution={false} = Pro white-label.",
  "author": "Spurvik (EyeTea Solutions ApS) <hello@spurvik.com>",
  "dependencies": [],
  "docs": "Render <Article50Badge systemName=\"Support Assistant\" /> where the notice should appear (or position=\"bottom-right\" to float). If you send a Content-Security-Policy, allow script-src https://spurvik.com. A visible notice supports your Art. 50 work; it is not Art. 50(2) machine-readable marking and not legal advice.",
  "categories": [
    "compliance",
    "ai"
  ],
  "files": [
    {
      "path": "components/article-50-badge.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\n/**\n * Spurvik Article 50 AI notice badge (EU AI Act Art. 50).\n *\n * Loads https://spurvik.com/badge.js (MIT, zero cookies, no telemetry, Shadow DOM)\n * once per page and renders the notice with its public create() API, so prop\n * changes update the badge and unmounting removes it.\n *\n *   <Article50Badge systemName=\"Support Assistant\" />                      // inline\n *   <Article50Badge systemName=\"Support Assistant\" position=\"bottom-right\" /> // floating\n *   <Article50Badge attribution={false} />  // Pro white-label: hides \"Art. 50 · Spurvik\"\n *\n * If your site sends a Content-Security-Policy, allow script-src https://spurvik.com.\n * A visible notice supports your Art. 50 transparency work. It does not add\n * Art. 50(2) machine-readable marking and is not legal advice.\n */\n\nexport type Article50Notice =\n  | \"interaction\"\n  | \"generated\"\n  | \"deepfake\"\n  | \"public-interest\"\n  | \"emotion-biometric\";\n\nexport type Article50Locale =\n  | \"bg\" | \"cs\" | \"da\" | \"de\" | \"el\" | \"en\" | \"es\" | \"et\" | \"fi\" | \"fr\" | \"ga\" | \"hr\"\n  | \"hu\" | \"it\" | \"lt\" | \"lv\" | \"mt\" | \"nl\" | \"pl\" | \"pt\" | \"ro\" | \"sk\" | \"sl\" | \"sv\";\n\nexport type Article50Theme = \"dark\" | \"light\" | \"auto\";\nexport type Article50Position = \"inline\" | \"bottom-right\" | \"bottom-left\";\n\nexport interface Article50BadgeProps {\n  /** Name shown after the notice, e.g. \"Support Assistant\". */\n  systemName?: string;\n  /** Which Art. 50 notice to show. Default \"interaction\". */\n  notice?: Article50Notice;\n  /** One of the 24 EU languages. Omit to follow <html lang>. */\n  locale?: Article50Locale;\n  /** Default \"dark\". \"auto\" follows prefers-color-scheme. */\n  theme?: Article50Theme;\n  /** \"inline\" renders where the component sits; the others float. Default \"inline\". */\n  position?: Article50Position;\n  /** Show the small \"Art. 50 · Spurvik\" link. Default true; false = Pro white-label. */\n  attribution?: boolean;\n  className?: string;\n}\n\nconst BADGE_SRC = \"https://spurvik.com/badge.js\";\n\ninterface SpurvikBadgeApi {\n  version: string;\n  create: (options: {\n    systemName?: string;\n    notice?: Article50Notice;\n    locale?: Article50Locale;\n    theme?: Article50Theme;\n    position?: Article50Position;\n    attribution?: boolean;\n  }) => HTMLElement;\n  mount: () => void;\n}\n\nfunction badgeApi(): SpurvikBadgeApi | undefined {\n  return (window as unknown as { SpurvikBadge?: SpurvikBadgeApi }).SpurvikBadge;\n}\n\nlet loading: Promise<SpurvikBadgeApi> | null = null;\n\nfunction loadBadge(): Promise<SpurvikBadgeApi> {\n  const ready = badgeApi();\n  if (ready) return Promise.resolve(ready);\n  if (loading) return loading;\n  loading = new Promise<SpurvikBadgeApi>((resolve, reject) => {\n    const existing = document.querySelector<HTMLScriptElement>(`script[src=\"${BADGE_SRC}\"]`);\n    const script = existing ?? document.createElement(\"script\");\n    script.addEventListener(\n      \"load\",\n      () => {\n        const api = badgeApi();\n        if (api) resolve(api);\n        else reject(new Error(\"badge.js loaded but window.SpurvikBadge is missing\"));\n      },\n      { once: true },\n    );\n    script.addEventListener(\n      \"error\",\n      () => {\n        loading = null;\n        reject(new Error(`Could not load ${BADGE_SRC}`));\n      },\n      { once: true },\n    );\n    if (!existing) {\n      script.src = BADGE_SRC;\n      script.async = true;\n      script.dataset.spurvikLoader = \"react\";\n      document.head.appendChild(script);\n    }\n  });\n  return loading;\n}\n\nexport function Article50Badge({\n  systemName,\n  notice = \"interaction\",\n  locale,\n  theme = \"dark\",\n  position = \"inline\",\n  attribution = true,\n  className,\n}: Article50BadgeProps) {\n  const slot = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    let host: HTMLElement | null = null;\n    let cancelled = false;\n    loadBadge()\n      .then((api) => {\n        if (cancelled) return;\n        host = api.create({ systemName, notice, locale, theme, position, attribution });\n        if (position === \"inline\") slot.current?.appendChild(host);\n        else document.body.appendChild(host);\n      })\n      .catch((error: unknown) => {\n        console.error(\"[article-50-badge]\", error);\n      });\n    return () => {\n      cancelled = true;\n      host?.remove();\n    };\n  }, [systemName, notice, locale, theme, position, attribution]);\n\n  // data-spurvik-mounted tells badge.js's auto-mount to leave this slot alone\n  // (no duplicate floating badge); the effect above renders into it instead.\n  return (\n    <div\n      ref={slot}\n      className={className}\n      data-spurvik-badge=\"\"\n      data-spurvik-mounted=\"react\"\n      hidden={position !== \"inline\"}\n    />\n  );\n}\n\nexport default Article50Badge;\n"
    }
  ]
}
