diff --git a/development/issues/closed/309.mjs b/development/issues/closed/309.mjs
index 31890c133475a945dbb7ac2f77d1de5036adfb3a..a873a39da87b15d4dced94e1e656958e2b38b6c9 100644
--- a/development/issues/closed/309.mjs
+++ b/development/issues/closed/309.mjs
@@ -14,6 +14,7 @@ import "../../../source/components/style/typography.pcss";
 import "../../../source/components/content/viewer.mjs";
 
 
+
 const v = document.getElementById("mainer");
 console.log(v)
 //
@@ -33,6 +34,10 @@ console.log(v)
 // const url = new URL(`data:text/markdown;base64,${btoa(markdownText)}`);
 // v.setContent(url, "text/markdown", "base64");
 
+const content = "zpl-demo"
+v.setContent(content, "application/oct33et-stream", "base64");
+
+
 //const url = new URL("data:text/plain;base64,SGVsbG8gV29ybGQh"); // base64 encoded string for "Hello World!"
 //v.setContent(url, "text/plain", "base64");
 
@@ -83,10 +88,10 @@ console.log(v)
 // Simple MP3 audio file encoded in base64
 // Minimal base64-encoded test video
 // Base64 encoded simple ping sound (44.1kHz, 16-bit, mono)
-const miniAudioBase64 = "SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4LjIwLjEwMAAAAAAAAAAAAAAA//uQZAAAAAAAAAAAAAAAAAAAAAAAWGluZwAAAA8AAAACAAACcQCA////////////AYCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID////////////////////////8AAAAZTGF2YzU4LjM1AAAAAAAAAAAAAAAAJAYAAAAAAAAAAnHyhqwaAAAAAAD/+7DEAAAHHA8iABBwALkNH1AiMmYiIn+JwQgkCYP4fwfBAMcEAQDgQg/B8//y4Pv//gg/+CAIcHwQDg+D4IAAAAD4IBgYGFAIUC4Pg+CAIAgCAIAuD//8HwQf//Ag/YEAwgwLhQDAoGIAz+MUzmX2bOuGicww1HUErAYcESGDJEKeLWg0Kw4EhwsShwWFDBgNB0BQoDgqGEwOAwIAYEQ0mOlI1JyBJI4/k6lQqEBYYgw8QkQzGAmPmyMpGR6RHCUBDgCGg4FwsJBYLBYBQ0RQGi0iJCwvNBdKnR4bwEXFxcXBMLhYLhcKhUJhITExISScTSQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//sQxNYDwAABpBwAACAAADSAAAAETEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU=";
-
-const url = new URL(`data:audio/mp3;base64,${miniAudioBase64}`);
-v.setContent(url, "audio/mp3", "base64");
+// const miniAudioBase64 = "SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4LjIwLjEwMAAAAAAAAAAAAAAA//uQZAAAAAAAAAAAAAAAAAAAAAAAWGluZwAAAA8AAAACAAACcQCA////////////AYCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID////////////////////////8AAAAZTGF2YzU4LjM1AAAAAAAAAAAAAAAAJAYAAAAAAAAAAnHyhqwaAAAAAAD/+7DEAAAHHA8iABBwALkNH1AiMmYiIn+JwQgkCYP4fwfBAMcEAQDgQg/B8//y4Pv//gg/+CAIcHwQDg+D4IAAAAD4IBgYGFAIUC4Pg+CAIAgCAIAuD//8HwQf//Ag/YEAwgwLhQDAoGIAz+MUzmX2bOuGicww1HUErAYcESGDJEKeLWg0Kw4EhwsShwWFDBgNB0BQoDgqGEwOAwIAYEQ0mOlI1JyBJI4/k6lQqEBYYgw8QkQzGAmPmyMpGR6RHCUBDgCGg4FwsJBYLBYBQ0RQGi0iJCwvNBdKnR4bwEXFxcXBMLhYLhcKhUJhITExISScTSQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//sQxNYDwAABpBwAACAAADSAAAAETEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU=";
+//
+// const url = new URL(`data:audio/mp3;base64,${miniAudioBase64}`);
+// v.setContent(url, "audio/mp3", "base64");
 
 // const minimalVideoBase64 = "AAAAIGZ0eXBpc29tAAACAGlzb21pc28yYXZjMW1wNDEAAAAIZnJlZQAAAu1tZGF0AAACoAYF//+c3EXpvebZSLeWLNgg2SPu73gyNjQgLSBjb3JlIDE0MiByMjQ3OSBkZDc5YTYxIC0gSC4yNjQvTVBFRy00IEFWQyBjb2RlYyAtIENvcHlsZWZ0IDIwMDMtMjAxNCAtIGh0dHA6Ly93d3cudmlkZW9sYW4ub3JnL3gyNjQuaHRtbCAtIG9wdGlvbnM6IGNhYmFjPTEgcmVmPTMgZGVibG9jaz0xOjA6MCBhbmFseXNlPTB4MzoweDExMyBtZT1oZXggc3VibWU9NyBwc3k9MSBwc3lfcmQ9MS4wMDowLjAwIG1peGVkX3JlZj0xIG1lX3JhbmdlPTE2IGNocm9tYV9tZT0xIHRyZWxsaXM9MSA4eDhkY3Q9MSBjcW09MCBkZWFkem9uZT0yMSwxMSBmYXN0X3Bza2lwPTEgY2hyb21hX3FwX29mZnNldD0tMiB0aHJlYWRzPTYgbG9va2FoZWFkX3RocmVhZHM9MSBzbGljZWRfdGhyZWFkcz0wIG5yPTAgZGVjaW1hdGU9MSBpbnRlcmxhY2VkPTAgYmx1cmF5X2NvbXBhdD0wIGNvbnN0cmFpbmVkX2ludHJhPTAgYmZyYW1lcz0zIGJfcHlyYW1pZD0yIGJfYWRhcHQ9MSBiX2JpYXM9MCBkaXJlY3Q9MSB3ZWlnaHRiPTEgb3Blbl9nb3A9MCB3ZWlnaHRwPTIga2V5aW50PTI1MCBrZXlpbnRfbWluPTEwIHNjZW5lY3V0PTQwIGludHJhX3JlZnJlc2g9MCByY19sb29rYWhlYWQ9NDAgcmM9Y3JmIG1idHJlZT0xIGNyZj0yMy4wIHFjb21wPTAuNjAgcXBtaW49MCBxcG1heD02OSBxcHN0ZXA9NCBpcF9yYXRpbz0xLjQwIGFxPTE6MS4wMACAAAAKYWVpegAVpD//+e5VrHBPqGP9IjQ1ErQWdyI5BQAAAAMAAAMAAANiAAA/AAADAAA6xQS1AAFr6v7oCKDAAAADOwAAAwAAAwB7AAA9AAAAAwAACywAAAAABowDGBHE84cFC6BnGJObADwQ6JCSNs5wOEMFLFNXBALRqRK33vh/+kRBagRa/qE/YqRQ+kRCAUW1ZUB/q2Qb+kR2uP8I/0h2vxj5oJAxM94/hYtpIFoAAAADAAADAAlYABO0AAADAAADABRWN3xTBGgTAAAABgAAAwUYLJwAAQBxtSxpJ1CZT4dCZ/6RUgRo9IiAAAAABgCAAAAXc3R0cwAAAAAAAAABAAAAAQAAAAAAAAAcc3RzYwAAAAAAAAABAAAAAQAAAAEAAAABAAAAFHN0c3oAAAAAAAAABAsAAAACAAAAFHN0Y28AAAAAAAAAAQAAAAEAAAABAAAAAQAAABRzdHNzAAAAAAAAAAEAAAABAAAAEHNzaWEAAAAAABBzc2QAAAAAAAAAAQAAAA8AAAACbWRpYQAAAABtZGhkAAAAABdoZGxyAAAAAAAAAAB2aWRlAAAAAAAAAAAAAAAAVmlkZW9IYW5kbGVyAAAAAmxtaW5mAAAAFHZtaGQAAAABAAAAAAAAAAAAAAAkZGluZgAAABxkcmVmAAAAAAAAAAEAAAAMdXJsIAAAAAEAAAIsc3RibAAAALRzdHNkAAAAAAAAAAEAAACkYXZjMQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAEYAcAASAAAAEgAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABj//wAAADNhdmNDAWQACv/hABlnZAAKrNlCmXv4wRAAAAAMAEAAAAMBA8SJmQEABGjxIf+AAAEaAAAADCBAKG5iKQABAAVo8SH/gAABGgAAAA5AQChOYilIAAAGaPEh/4AAARoAAAAPAEAoTmIpAAAAGj8IIAIQEBYQAAAAGHhwYWMAAAAAAAAAAgAAAAEAABAOAAAAABxhdmMxAAAAAAAAAgAAAAEAABAsAAAAAAEYAXAAAABIAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY//8AAAAzYXZjQwFkAAr/4QAZZmQACqzZQpl7+MEQAAAACXwAAAgIDC9mAAABGjxIf+AAAEaAAAAMEEAobmIpAAEABWjxIf+AAAEaAAAADkBAKE5iKUgAAAZo8SH/gAABGgAAAA8AQChOYikAAAAaPwggAhAQFhAAAAAYeHBhYwAAAAAAAAACAAAAAwAAEA4AAAAAHGaDLZVzJGhE";
 //
diff --git a/source/components/content/viewer.mjs b/source/components/content/viewer.mjs
index 039ae12227fe8fa7c7c5d669cd968137c6c6b787..9c6066a488b2933fc683075014ae7018047c4441 100644
--- a/source/components/content/viewer.mjs
+++ b/source/components/content/viewer.mjs
@@ -13,21 +13,24 @@
  */
 
 import {
-	assembleMethodSymbol,
-	CustomElement,
-	registerCustomElement,
+    assembleMethodSymbol,
+    CustomElement,
+    registerCustomElement,
 } from "../../dom/customelement.mjs";
 import "../notify/notify.mjs";
-import { ViewerStyleSheet } from "./stylesheet/viewer.mjs";
-import { instanceSymbol } from "../../constants.mjs";
-import { isString } from "../../types/is.mjs";
-import { getGlobal } from "../../types/global.mjs";
-import { MediaType, parseMediaType } from "../../types/mediatype.mjs";
-import { MarkdownToHTML } from "../../text/markdown-parser.mjs";
+import {ViewerStyleSheet} from "./stylesheet/viewer.mjs";
+import {instanceSymbol} from "../../constants.mjs";
+import {isString} from "../../types/is.mjs";
+import {getGlobal} from "../../types/global.mjs";
+import {MediaType, parseMediaType} from "../../types/mediatype.mjs";
+import {MarkdownToHTML} from "../../text/markdown-parser.mjs";
 import "../layout/tabs.mjs";
 import "./viewer/message.mjs";
+import {getLocaleOfDocument} from "../../dom/locale.mjs";
+import {Button} from "../form/button.mjs";
+import {findTargetElementFromEvent} from "../../dom/events.mjs";
 
-export { Viewer };
+export {Viewer};
 
 /**
  * @private
@@ -48,604 +51,657 @@ const viewerElementSymbol = Symbol("viewerElement");
  * @summary A simple viewer component for PDF, HTML, and images.
  */
 class Viewer extends CustomElement {
-	/**
-	 * This method is called by the `instanceof` operator.
-	 * @return {symbol}
-	 */
-	static get [instanceSymbol]() {
-		return Symbol.for("@schukai/monster/components/content/viewer@@instance");
-	}
-
-	/**
-	 * To set the options via the HTML tag, the attribute `data-monster-options` must be used.
-	 * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
-	 *
-	 * The individual configuration values can be found in the table.
-	 *
-	 * @property {Object} templates Template definitions
-	 * @property {string} templates.main Main template
-	 * @property {string} content Content to be displayed in the viewer
-	 * @property {Object} classes Css classes
-	 * @property {string} classes.viewer Css class for the viewer
-	 * @property {Object} renderers Renderers for different media types
-	 * @property {function} renderers.image Function to render image content
-	 * @property {function} renderers.html Function to render HTML content
-	 * @property {function} renderers.pdf Function to render PDF content
-	 * @property {function} renderers.plaintext Function to render plain text content
-	 * @property {function} renderers.markdown Function to render Markdown content
-	 */
-	get defaults() {
-		return Object.assign({}, super.defaults, {
-			templates: {
-				main: getTemplate(),
-			},
-			content: "<slot></slot>",
-			classes: {
-				viewer: "",
-			},
-			renderers: {
-				image: this.setImage,
-				html: this.setHTML,
-				pdf: this.setPDF,
-				plaintext: this.setPlainText,
-				markdown: this.setMarkdown,
-				audio: this.setAudio,
-				video: this.setVideo,
-				message: this.setMessage,
-			},
-		});
-	}
-
-	/**
-	 * Sets the content of an element based on the provided content and media type.
-	 *
-	 * @param {string} content - The content to be set.
-	 * @param {string} [mediaType="text/plain"] - The media type of the content. Defaults to "text/plain" if not specified.
-	 * @return {void} This method does not return a value.
-	 * @throws {Error} Throws an error if shadowRoot is not defined.
-	 */
-	setContent(content, mediaType = "text/plain") {
-		if (!this.shadowRoot) {
-			throw new Error("no shadow-root is defined");
-		}
-
-		const renderers = this.getOption("renderers");
-
-		const isDataURL = (value) => {
-			return (
-				(typeof value === "string" && value.startsWith("data:")) ||
-				(value instanceof URL && value.protocol === "data:")
-			);
-		};
-
-		if (isDataURL(content)) {
-			try {
-				const dataUrl = content.toString();
-				const [header] = dataUrl.split(",");
-				const [typeSegment] = header.split(";");
-				mediaType = typeSegment.replace("data:", "") || "text/plain";
-			} catch (error) {
-				this.dispatchEvent(
-					new CustomEvent("viewer-error", {
-						detail: "Invalid data URL format",
-					}),
-				);
-				return;
-			}
-		}
-
-		if (mediaType === undefined || mediaType === null || mediaType === "") {
-			mediaType = "text/plain";
-		}
-
-		let mediaTypeObject;
-
-		try {
-			mediaTypeObject = new parseMediaType(mediaType);
-			if (!(mediaTypeObject instanceof MediaType)) {
-				this.dispatchEvent(
-					new CustomEvent("viewer-error", { detail: "Invalid MediaType" }),
-				);
-				return;
-			}
-		} catch (error) {
-			this.dispatchEvent(new CustomEvent("viewer-error", { detail: error }));
-			return;
-		}
-
-		const checkRenderer = (renderer, contentType) => {
-			if (renderers && typeof renderers[renderer] === "function") {
-				return true;
-			} else {
-				this.dispatchEvent(
-					new CustomEvent("viewer-error", {
-						detail: `Renderer for ${contentType} not found`,
-					}),
-				);
-				return false;
-			}
-		};
-
-		switch (mediaTypeObject.type) {
-			case "text":
-				switch (mediaTypeObject.subtype) {
-					case "html":
-						if (checkRenderer("html", mediaTypeObject.toString())) {
-							renderers.html.call(this, content);
-						}
-						break;
-					case "plain":
-						if (checkRenderer("plaintext", mediaTypeObject.toString())) {
-							renderers.plaintext.call(this, content);
-						}
-						break;
-					case "markdown":
-						if (checkRenderer("markdown", mediaTypeObject.toString())) {
-							this.setMarkdown(content);
-						}
-						break;
-					default:
-						if (checkRenderer("plaintext", mediaTypeObject.toString())) {
-							renderers.plaintext.call(this, content);
-						}
-						break;
-				}
-				break;
-
-			case "application":
-				switch (mediaTypeObject.subtype) {
-					case "pdf":
-						if (checkRenderer("pdf", mediaTypeObject.toString())) {
-							renderers.pdf.call(this, content);
-						}
-						break;
-
-					default:
-						this.setOption("content", content);
-						break;
-				}
-				break;
-
-			case "message":
-				switch (mediaTypeObject.subtype) {
-					case "rfc822":
-						if (checkRenderer("message", mediaTypeObject.toString())) {
-							renderers.message.call(this, content);
-						}
-						break;
-
-					default:
-						this.setOption("content", content);
-						break;
-				}
-				break;
-
-			case "audio":
-				if (checkRenderer(mediaTypeObject.type, mediaTypeObject.toString())) {
-					renderers[mediaTypeObject.type].call(this, content);
-				}
-				break;
-
-			case "video":
-				if (checkRenderer(mediaTypeObject.type, mediaTypeObject.toString())) {
-					renderers[mediaTypeObject.type].call(this, content);
-				}
-				break;
-
-			case "image":
-				if (checkRenderer("image", mediaTypeObject.toString())) {
-					renderers.image.call(this, content);
-				}
-				break;
-
-			default:
-				this.setOption("content", content);
-				this.dispatchEvent(
-					new CustomEvent("viewer-error", {
-						detail: `Unsupported media type: ${mediaTypeObject.toString()}`,
-					}),
-				); // Notify about unsupported media type
-				return;
-		}
-	}
-
-	/**
-	 * Sets the audio content for the viewer. Accepts a Blob, URL, or string and processes it
-	 * to configure audio playback within the viewer. Throws an error if the input type is invalid.
-	 *
-	 * @param {Blob|string} data - The audio content. This can be a Blob, a URL, or a string.
-	 * @return {void} No return value.
-	 */
-	setAudio(data) {
-		if (isBlob(data)) {
-			data = URL.createObjectURL(data);
-		} else if (isURL(data)) {
-			// nothing to do
-		} else if (isString(data)) {
-			// nothing to do
-		} else {
-			this.dispatchEvent(
-				new CustomEvent("viewer-error", { detail: "Blob or URL expected" }),
-			);
-			throw new Error("Blob or URL expected");
-		}
-
-		this.setOption(
-			"content",
-			`
+    /**
+     * This method is called by the `instanceof` operator.
+     * @return {symbol}
+     */
+    static get [instanceSymbol]() {
+        return Symbol.for("@schukai/monster/components/content/viewer@@instance");
+    }
+
+    /**
+     * To set the options via the HTML tag, the attribute `data-monster-options` must be used.
+     * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
+     *
+     * The individual configuration values can be found in the table.
+     *
+     * @property {Object} templates Template definitions
+     * @property {string} templates.main Main template
+     * @property {string} content Content to be displayed in the viewer
+     * @property {Object} classes Css classes
+     * @property {string} classes.viewer Css class for the viewer
+     * @property {Object} renderers Renderers for different media types
+     * @property {function} renderers.image Function to render image content
+     * @property {function} renderers.html Function to render HTML content
+     * @property {function} renderers.pdf Function to render PDF content
+     * @property {function} renderers.plaintext Function to render plain text content
+     * @property {function} renderers.markdown Function to render Markdown content
+     */
+    get defaults() {
+        return Object.assign({}, super.defaults, {
+            templates: {
+                main: getTemplate(),
+            },
+            content: "<slot></slot>",
+            classes: {
+                viewer: "",
+            },
+            labels: getLabels(),
+            renderers: {
+                image: this.setImage,
+                html: this.setHTML,
+                pdf: this.setPDF,
+                download: this.setDownload,
+                plaintext: this.setPlainText,
+                markdown: this.setMarkdown,
+                audio: this.setAudio,
+                video: this.setVideo,
+                message: this.setMessage,
+            },
+
+        });
+    }
+
+    /**
+     * Sets the content of an element based on the provided content and media type.
+     *
+     * @param {string} content - The content to be set.
+     * @param {string} [mediaType="text/plain"] - The media type of the content. Defaults to "text/plain" if not specified.
+     * @return {void} This method does not return a value.
+     * @throws {Error} Throws an error if shadowRoot is not defined.
+     */
+    setContent(content, mediaType = "text/plain") {
+        if (!this.shadowRoot) {
+            throw new Error("no shadow-root is defined");
+        }
+
+        const renderers = this.getOption("renderers");
+
+        const isDataURL = (value) => {
+            return (
+                (typeof value === "string" && value.startsWith("data:")) ||
+                (value instanceof URL && value.protocol === "data:")
+            );
+        };
+
+        if (isDataURL(content)) {
+            try {
+                const dataUrl = content.toString();
+                const [header] = dataUrl.split(",");
+                const [typeSegment] = header.split(";");
+                mediaType = typeSegment.replace("data:", "") || "text/plain";
+            } catch (error) {
+                this.dispatchEvent(
+                    new CustomEvent("viewer-error", {
+                        detail: "Invalid data URL format",
+                    }),
+                );
+                return;
+            }
+        }
+
+        if (mediaType === undefined || mediaType === null || mediaType === "") {
+            mediaType = "text/plain";
+        }
+
+        let mediaTypeObject;
+
+        try {
+            mediaTypeObject = new parseMediaType(mediaType);
+            if (!(mediaTypeObject instanceof MediaType)) {
+                this.dispatchEvent(
+                    new CustomEvent("viewer-error", {detail: "Invalid MediaType"}),
+                );
+                return;
+            }
+        } catch (error) {
+            this.dispatchEvent(new CustomEvent("viewer-error", {detail: error}));
+            return;
+        }
+
+        const checkRenderer = (renderer, contentType) => {
+            if (renderers && typeof renderers[renderer] === "function") {
+                return true;
+            } else {
+                this.dispatchEvent(
+                    new CustomEvent("viewer-error", {
+                        detail: `Renderer for ${contentType} not found`,
+                    }),
+                );
+                return false;
+            }
+        };
+
+        switch (mediaTypeObject.type) {
+            case "text":
+                switch (mediaTypeObject.subtype) {
+                    case "html":
+                        if (checkRenderer("html", mediaTypeObject.toString())) {
+                            renderers.html.call(this, content);
+                        }
+                        break;
+                    case "plain":
+                        if (checkRenderer("plaintext", mediaTypeObject.toString())) {
+                            renderers.plaintext.call(this, content);
+                        }
+                        break;
+                    case "markdown":
+                        if (checkRenderer("markdown", mediaTypeObject.toString())) {
+                            this.setMarkdown(content);
+                        }
+                        break;
+                    default:
+                        if (checkRenderer("plaintext", mediaTypeObject.toString())) {
+                            renderers.plaintext.call(this, content);
+                        }
+                        break;
+                }
+                break;
+
+            case "application":
+                switch (mediaTypeObject.subtype) {
+                    case "pdf":
+                        if (checkRenderer("pdf", mediaTypeObject.toString())) {
+                            renderers.pdf.call(this, content);
+                        }
+                        break;
+
+                    default:
+
+                        // Handle octet-stream as a generic binary data type
+                        if (checkRenderer("download", mediaTypeObject.toString())) {
+                            renderers.download.call(this, content);
+                            break;
+                        }
+
+                        this.setOption("content", content + "!!");
+                        break;
+                }
+                break;
+
+            case "message":
+                switch (mediaTypeObject.subtype) {
+                    case "rfc822":
+                        if (checkRenderer("message", mediaTypeObject.toString())) {
+                            renderers.message.call(this, content);
+                        }
+                        break;
+
+                    default:
+                        this.setOption("content", content);
+                        break;
+                }
+                break;
+
+            case "audio":
+                if (checkRenderer(mediaTypeObject.type, mediaTypeObject.toString())) {
+                    renderers[mediaTypeObject.type].call(this, content);
+                }
+                break;
+
+            case "video":
+                if (checkRenderer(mediaTypeObject.type, mediaTypeObject.toString())) {
+                    renderers[mediaTypeObject.type].call(this, content);
+                }
+                break;
+
+            case "image":
+                if (checkRenderer("image", mediaTypeObject.toString())) {
+                    renderers.image.call(this, content);
+                }
+                break;
+
+            default:
+                this.setOption("content", content);
+                this.dispatchEvent(
+                    new CustomEvent("viewer-error", {
+                        detail: `Unsupported media type: ${mediaTypeObject.toString()}`,
+                    }),
+                ); // Notify about unsupported media type
+                return;
+        }
+    }
+
+    /**
+     * Sets the audio content for the viewer. Accepts a Blob, URL, or string and processes it
+     * to configure audio playback within the viewer. Throws an error if the input type is invalid.
+     *
+     * @param {Blob|string} data - The audio content. This can be a Blob, a URL, or a string.
+     * @return {void} No return value.
+     */
+    setAudio(data) {
+        if (isBlob(data)) {
+            data = URL.createObjectURL(data);
+        } else if (isURL(data)) {
+            // nothing to do
+        } else if (isString(data)) {
+            // nothing to do
+        } else {
+            this.dispatchEvent(
+                new CustomEvent("viewer-error", {detail: "Blob or URL expected"}),
+            );
+            throw new Error("Blob or URL expected");
+        }
+
+        this.setOption(
+            "content",
+            `
                    <audio controls part="audio" style="max-width: 100%">
                        <source src="${data}">
                    </audio>`,
-		);
-	}
-
-	/**
-	 * Sets the video content for the viewer. The method accepts a Blob, URL, or string,
-	 * verifies its type, and updates the viewer's content accordingly.
-	 *
-	 * @param {Blob|string} data - The video data to set. It can be a Blob, URL, or string.
-	 * @return {void} This method does not return a value. It updates the viewer's state.
-	 * @throws {Error} Throws an error if the provided data is not a Blob or URL.
-	 */
-	setVideo(data) {
-		if (isBlob(data)) {
-			data = URL.createObjectURL(data);
-		} else if (isURL(data)) {
-			// nothing to do
-		} else if (isString(data)) {
-			// nothing to do
-		} else {
-			this.dispatchEvent(
-				new CustomEvent("viewer-error", { detail: "Blob or URL expected" }),
-			);
-			throw new Error("Blob or URL expected");
-		}
-
-		this.setOption(
-			"content",
-			`
+        );
+    }
+
+    /**
+     * Sets the video content for the viewer. The method accepts a Blob, URL, or string,
+     * verifies its type, and updates the viewer's content accordingly.
+     *
+     * @param {Blob|string} data - The video data to set. It can be a Blob, URL, or string.
+     * @return {void} This method does not return a value. It updates the viewer's state.
+     * @throws {Error} Throws an error if the provided data is not a Blob or URL.
+     */
+    setVideo(data) {
+        if (isBlob(data)) {
+            data = URL.createObjectURL(data);
+        } else if (isURL(data)) {
+            // nothing to do
+        } else if (isString(data)) {
+            // nothing to do
+        } else {
+            this.dispatchEvent(
+                new CustomEvent("viewer-error", {detail: "Blob or URL expected"}),
+            );
+            throw new Error("Blob or URL expected");
+        }
+
+        this.setOption(
+            "content",
+            `
                    <video controls part="video" style="max-width: 100%">
                      <source src="${data}">
                    </video>`,
-		);
-	}
-
-	/**
-	 * Renders Markdown content using built-in or custom Markdown parser.
-	 * Overrideable via `customRenderers['text/markdown']`.
-	 *
-	 * @param {string|Blob} data
-	 */
-	setMarkdown(data) {
-		if (isBlob(data)) {
-			blobToText(data)
-				.then((markdownText) => {
-					try {
-						const html = MarkdownToHTML.convert(markdownText);
-						this.setHTML(html);
-					} catch (error) {
-						this.setPlainText(markdownText); // Fallback to plain text if conversion fails
-						this.dispatchEvent(
-							new CustomEvent("viewer-error", { detail: error }),
-						);
-					}
-				})
-				.catch((error) => {
-					this.dispatchEvent(
-						new CustomEvent("viewer-error", { detail: error }),
-					);
-					throw new Error(error);
-				});
-			return;
-		} else if (isURL(data)) {
-			getGlobal()
-				.fetch(data)
-				.then((response) => {
-					return response.text();
-				})
-				.then((markdownText) => {
-					try {
-						const html = MarkdownToHTML.convert(markdownText);
-						this.setHTML(html);
-					} catch (error) {
-						this.setPlainText(markdownText); // Fallback to plain text if conversion fails
-						this.dispatchEvent(
-							new CustomEvent("viewer-error", { detail: error }),
-						);
-					}
-				})
-				.catch((error) => {
-					this.dispatchEvent(
-						new CustomEvent("viewer-error", { detail: error }),
-					);
-					throw new Error(error);
-				});
-			return;
-		} else if (isString(data)) {
-			try {
-				const html = MarkdownToHTML.convert(data);
-				this.setHTML(html);
-			} catch (error) {
-				this.setPlainText(data); // Fallback to plain text if conversion fails
-				this.dispatchEvent(new CustomEvent("viewer-error", { detail: error }));
-			}
-			return;
-		}
-
-		this.dispatchEvent(
-			new CustomEvent("viewer-error", { detail: "Blob or string expected" }),
-		);
-		throw new Error("Blob or string expected");
-	}
-
-	/**
-	 * Configures and embeds a PDF document into the application with customizable display settings.
-	 *
-	 * @param {Blob|URL|string} data The PDF data to be embedded. Can be provided as a Blob, URL, or base64 string.
-	 * @param {boolean} [navigation=true] Determines whether the navigation pane is displayed in the PDF viewer.
-	 * @param {boolean} [toolbar=true] Controls the visibility of the toolbar in the PDF viewer.
-	 * @param {boolean} [scrollbar=false] Configures the display of the scrollbar in the PDF viewer.
-	 * @return {void} This method returns nothing but sets the embedded PDF as the content.
-	 */
-	setPDF(data, navigation = true, toolbar = true, scrollbar = false) {
-		const hashes =
-			"#toolbar=" +
-			(toolbar ? "1" : "0") +
-			"&navpanes=" +
-			(navigation ? "1" : "0") +
-			"&scrollbar=" +
-			(scrollbar ? "1" : "0");
-
-		let pdfURL = "";
-		if (isBlob(data)) {
-			pdfURL = URL.createObjectURL(data);
-			pdfURL += hashes;
-		} else if (isURL(data)) {
-			// check if the url already contains the hashes
-			if (data?.hash?.indexOf("#") === -1) {
-				pdfURL = data.toString() + hashes;
-			} else {
-				pdfURL = data.toString();
-			}
-		} else if (isString(data)) {
-			//URL.createObjectURL(data);
-			const blobObj = new Blob([atob(data)], { type: "application/pdf" });
-			const url = window.URL.createObjectURL(blobObj);
-
-			pdfURL = data;
-		} else {
-			this.dispatchEvent(
-				new CustomEvent("viewer-error", { detail: "Blob or URL expected" }),
-			);
-			throw new Error("Blob or URL expected");
-		}
-
-		const html =
-			'<object part="pdf" data="' +
-			pdfURL +
-			'" width="100%" height="100%" type="application/pdf"></object>';
-
-		this.setOption("content", html);
-	}
-
-	/**
-	 * Sets the content for displaying an email message.
-	 * The data is expected to be an object with a structure containing
-	 * 'to', 'from', 'subject', 'parts', and 'headers'.
-	 * The parts are processed to display plain text and HTML in separate tabs,
-	 * and attachments are listed.
-	 *
-	 * @param {object} emailData - The structured email data.
-	 */
-	setMessage(emailData) {
-		if (!emailData || typeof emailData !== "object") {
-			this.dispatchEvent(
-				new CustomEvent("viewer-error", {
-					detail: "Invalid email data provided",
-				}),
-			);
-			return;
-		}
-
-		this.setOption(
-			"content",
-			'<monster-message-content part="message"></monster-message-content>',
-		);
-
-		setTimeout(() => {
-			const messageContent = this.shadowRoot.querySelector(
-				"monster-message-content",
-			);
-			if (!messageContent) {
-				this.dispatchEvent(
-					new CustomEvent("viewer-error", {
-						detail: "Message content element not found",
-					}),
-				);
-				return;
-			}
-
-			messageContent.setMessage(emailData);
-		}, 100);
-	}
-
-	/**
-	 * Sets an image for the target by accepting a blob, URL, or string representation of the image.
-	 *
-	 * @param {(Blob|string)} data - The image data, which can be a Blob, a valid URL, or a string representation of the image.
-	 * @return {void} Does not return a value.
-	 */
-	setImage(data) {
-		if (isBlob(data)) {
-			data = URL.createObjectURL(data);
-		} else if (isURL(data)) {
-			// nothing to do
-		} else if (isString(data)) {
-			// nothing to do
-		} else {
-			this.dispatchEvent(
-				new CustomEvent("viewer-error", { detail: "Blob or URL expected" }),
-			);
-			throw new Error("Blob or URL expected");
-		}
-
-		this.setOption(
-			"content",
-			`<img style="max-width: 100%" src="${data}" alt="image" part="image" onerror="this.dispatchEvent(new CustomEvent('viewer-error', {detail: 'Image loading error'}));">`,
-		);
-	}
-
-	/**
-	 *
-	 * if the data is a string, it is interpreted as HTML.
-	 * if the data is a URL, the HTML is loaded from the url and set as content.
-	 * if the data is an HTMLElement, the outerHTML is used as content.
-	 *
-	 * @param {HTMLElement|URL|string|Blob} data
-	 */
-	setHTML(data) {
-		if (data instanceof Blob) {
-			blobToText(data)
-				.then((html) => {
-					this.setOption("content", html);
-				})
-				.catch((error) => {
-					this.dispatchEvent(
-						new CustomEvent("viewer-error", { detail: error }),
-					);
-					throw new Error(error);
-				});
-
-			return;
-		} else if (data instanceof HTMLElement) {
-			data = data.outerHTML;
-		} else if (isString(data)) {
-			// nothing to do
-		} else if (isURL(data)) {
-			// fetch element
-			getGlobal()
-				.fetch(data)
-				.then((response) => {
-					return response.text();
-				})
-				.then((html) => {
-					this.setOption("content", html);
-				})
-				.catch((error) => {
-					this.dispatchEvent(
-						new CustomEvent("viewer-error", { detail: error }),
-					);
-					throw new Error(error);
-				});
-		} else {
-			this.dispatchEvent(
-				new CustomEvent("viewer-error", {
-					detail: "HTMLElement or string expected",
-				}),
-			);
-			throw new Error("HTMLElement or string expected");
-		}
-
-		this.setOption("content", data);
-	}
-
-	/**
-	 * Sets the plain text content by processing the input data, which can be of various types, including Blob,
-	 * HTMLElement, string, or a valid URL. The method extracts and sets the raw text content into a predefined option.
-	 *
-	 * @param {Blob|HTMLElement|string} data - The input data to be processed. It can be a Blob object, an HTMLElement,
-	 * a plain string, or a string formatted as a valid URL. The method determines
-	 * the data type and processes it accordingly.
-	 * @return {void} - This method does not return any value. It processes the content and updates the relevant option
-	 * property.
-	 */
-	setPlainText(data) {
-		const mkPreSpan = (text) => {
-			const pre = document.createElement("pre");
-			pre.innerText = text;
-			pre.setAttribute("part", "text");
-			return pre.outerHTML;
-		};
-
-		if (data instanceof Blob) {
-			blobToText(data)
-				.then((text) => {
-					const div = document.createElement("div");
-					div.innerHTML = text;
-					text = div.innerText;
-
-					this.setOption("content", mkPreSpan(text));
-				})
-				.catch((error) => {
-					this.dispatchEvent(
-						new CustomEvent("viewer-error", { detail: error }),
-					);
-					throw new Error(error);
-				});
-
-			return;
-		} else if (data instanceof HTMLElement) {
-			data = data.outerText;
-		} else if (isString(data)) {
-			const div = document.createElement("div");
-			div.innerHTML = data;
-			data = div.innerText;
-		} else if (isURL(data)) {
-			getGlobal()
-				.fetch(data)
-				.then((response) => {
-					return response.text();
-				})
-				.then((text) => {
-					const div = document.createElement("div");
-					div.innerHTML = text;
-					text = div.innerText;
-
-					this.setOption("content", mkPreSpan(text));
-				})
-				.catch((error) => {
-					this.dispatchEvent(
-						new CustomEvent("viewer-error", { detail: error }),
-					);
-					throw new Error(error);
-				});
-		} else {
-			this.dispatchEvent(
-				new CustomEvent("viewer-error", {
-					detail: "HTMLElement or string expected",
-				}),
-			);
-			throw new Error("HTMLElement or string expected");
-		}
-
-		this.setOption("content", mkPreSpan(data));
-	}
-
-	/**
-	 *
-	 * @return {Viewer}
-	 */
-	[assembleMethodSymbol]() {
-		super[assembleMethodSymbol]();
-
-		initControlReferences.call(this);
-		initEventHandler.call(this);
-	}
-
-	/**
-	 *
-	 * @return {string}
-	 */
-	static getTag() {
-		return "monster-viewer";
-	}
-
-	/**
-	 * @return {CSSStyleSheet[]}
-	 */
-	static getCSSStyleSheet() {
-		return [ViewerStyleSheet];
-	}
+        );
+    }
+
+    /**
+     * Renders Markdown content using built-in or custom Markdown parser.
+     * Overrideable via `customRenderers['text/markdown']`.
+     *
+     * @param {string|Blob} data
+     */
+    setMarkdown(data) {
+        if (isBlob(data)) {
+            blobToText(data)
+                .then((markdownText) => {
+                    try {
+                        const html = MarkdownToHTML.convert(markdownText);
+                        this.setHTML(html);
+                    } catch (error) {
+                        this.setPlainText(markdownText); // Fallback to plain text if conversion fails
+                        this.dispatchEvent(
+                            new CustomEvent("viewer-error", {detail: error}),
+                        );
+                    }
+                })
+                .catch((error) => {
+                    this.dispatchEvent(
+                        new CustomEvent("viewer-error", {detail: error}),
+                    );
+                    throw new Error(error);
+                });
+            return;
+        } else if (isURL(data)) {
+            getGlobal()
+                .fetch(data)
+                .then((response) => {
+                    return response.text();
+                })
+                .then((markdownText) => {
+                    try {
+                        const html = MarkdownToHTML.convert(markdownText);
+                        this.setHTML(html);
+                    } catch (error) {
+                        this.setPlainText(markdownText); // Fallback to plain text if conversion fails
+                        this.dispatchEvent(
+                            new CustomEvent("viewer-error", {detail: error}),
+                        );
+                    }
+                })
+                .catch((error) => {
+                    this.dispatchEvent(
+                        new CustomEvent("viewer-error", {detail: error}),
+                    );
+                    throw new Error(error);
+                });
+            return;
+        } else if (isString(data)) {
+            try {
+                const html = MarkdownToHTML.convert(data);
+                this.setHTML(html);
+            } catch (error) {
+                this.setPlainText(data); // Fallback to plain text if conversion fails
+                this.dispatchEvent(new CustomEvent("viewer-error", {detail: error}));
+            }
+            return;
+        }
+
+        this.dispatchEvent(
+            new CustomEvent("viewer-error", {detail: "Blob or string expected"}),
+        );
+        throw new Error("Blob or string expected");
+    }
+
+    /**
+     * Configures and embeds a PDF document into the application with customizable display settings.
+     *
+     * @param {Blob|URL|string} data The PDF data to be embedded. Can be provided as a Blob, URL, or base64 string.
+     * @param {boolean} [navigation=true] Determines whether the navigation pane is displayed in the PDF viewer.
+     * @param {boolean} [toolbar=true] Controls the visibility of the toolbar in the PDF viewer.
+     * @param {boolean} [scrollbar=false] Configures the display of the scrollbar in the PDF viewer.
+     * @return {void} This method returns nothing but sets the embedded PDF as the content.
+     */
+    setPDF(data, navigation = true, toolbar = true, scrollbar = false) {
+        const hashes =
+            "#toolbar=" +
+            (toolbar ? "1" : "0") +
+            "&navpanes=" +
+            (navigation ? "1" : "0") +
+            "&scrollbar=" +
+            (scrollbar ? "1" : "0");
+
+        let pdfURL = "";
+        if (isBlob(data)) {
+            pdfURL = URL.createObjectURL(data);
+            pdfURL += hashes;
+        } else if (isURL(data)) {
+            // check if the url already contains the hashes
+            if (data?.hash?.indexOf("#") === -1) {
+                pdfURL = data.toString() + hashes;
+            } else {
+                pdfURL = data.toString();
+            }
+        } else if (isString(data)) {
+            //URL.createObjectURL(data);
+            const blobObj = new Blob([atob(data)], {type: "application/pdf"});
+            const url = window.URL.createObjectURL(blobObj);
+
+            pdfURL = data;
+        } else {
+            this.dispatchEvent(
+                new CustomEvent("viewer-error", {detail: "Blob or URL expected"}),
+            );
+            throw new Error("Blob or URL expected");
+        }
+
+        const html =
+            '<object part="pdf" data="' +
+            pdfURL +
+            '" width="100%" height="100%" type="application/pdf"></object>';
+
+        this.setOption("content", html);
+    }
+
+    /**
+     * Sets the download functionality for the viewer.
+     * @param data
+     * @param filename
+     */
+    setDownload(data, filename = "download") {
+
+        const rawData = data;
+
+        if (isBlob(data)) {
+            data = URL.createObjectURL(data);
+        } else if (isURL(data)) {
+            // nothing to do
+        } else if (isString(data)) {
+            // nothing to do
+        } else {
+            this.dispatchEvent(
+                new CustomEvent("viewer-error", {detail: "Blob or URL expected"}),
+            );
+            throw new Error("Blob or URL expected");
+        }
+
+        const button = `<monster-button data-monster-role="download">` + this.getOption('labels.download') + `</monster-button>`;
+        this.setOption("content", button);
+
+        this.addEventListener("click", (event) => {
+
+            const element = findTargetElementFromEvent(event, "data-monster-role", "download");
+            if (element instanceof Button) {
+                const anchor = document.createElement("a");
+                anchor.href = URL.createObjectURL(new Blob([rawData]))
+                anchor.download = filename;
+                anchor.style.display = "none";
+                document.body.appendChild(anchor);
+                anchor.click();
+                document.body.removeChild(anchor);
+            }
+
+
+        })
+
+    }
+
+    /**
+     * Sets the content for displaying an email message.
+     * The data is expected to be an object with a structure containing
+     * 'to', 'from', 'subject', 'parts', and 'headers'.
+     * The parts are processed to display plain text and HTML in separate tabs,
+     * and attachments are listed.
+     *
+     * @param {object} emailData - The structured email data.
+     */
+    setMessage(emailData) {
+        if (!emailData || typeof emailData !== "object") {
+            this.dispatchEvent(
+                new CustomEvent("viewer-error", {
+                    detail: "Invalid email data provided",
+                }),
+            );
+            return;
+        }
+
+        this.setOption(
+            "content",
+            '<monster-message-content part="message"></monster-message-content>',
+        );
+
+        setTimeout(() => {
+            const messageContent = this.shadowRoot.querySelector(
+                "monster-message-content",
+            );
+            if (!messageContent) {
+                this.dispatchEvent(
+                    new CustomEvent("viewer-error", {
+                        detail: "Message content element not found",
+                    }),
+                );
+                return;
+            }
+
+            messageContent.setMessage(emailData);
+        }, 100);
+    }
+
+    /**
+     * Sets an image for the target by accepting a blob, URL, or string representation of the image.
+     *
+     * @param {(Blob|string)} data - The image data, which can be a Blob, a valid URL, or a string representation of the image.
+     * @return {void} Does not return a value.
+     */
+    setImage(data) {
+        if (isBlob(data)) {
+            data = URL.createObjectURL(data);
+        } else if (isURL(data)) {
+            // nothing to do
+        } else if (isString(data)) {
+            // nothing to do
+        } else {
+            this.dispatchEvent(
+                new CustomEvent("viewer-error", {detail: "Blob or URL expected"}),
+            );
+            throw new Error("Blob or URL expected");
+        }
+
+        this.setOption(
+            "content",
+            `<img style="max-width: 100%" src="${data}" alt="image" part="image" onerror="this.dispatchEvent(new CustomEvent('viewer-error', {detail: 'Image loading error'}));">`,
+        );
+    }
+
+    /**
+     *
+     * if the data is a string, it is interpreted as HTML.
+     * if the data is a URL, the HTML is loaded from the url and set as content.
+     * if the data is an HTMLElement, the outerHTML is used as content.
+     *
+     * @param {HTMLElement|URL|string|Blob} data
+     */
+    setHTML(data) {
+        if (data instanceof Blob) {
+            blobToText(data)
+                .then((html) => {
+                    this.setOption("content", html);
+                })
+                .catch((error) => {
+                    this.dispatchEvent(
+                        new CustomEvent("viewer-error", {detail: error}),
+                    );
+                    throw new Error(error);
+                });
+
+            return;
+        } else if (data instanceof HTMLElement) {
+            data = data.outerHTML;
+        } else if (isString(data)) {
+            // nothing to do
+        } else if (isURL(data)) {
+            // fetch element
+            getGlobal()
+                .fetch(data)
+                .then((response) => {
+                    return response.text();
+                })
+                .then((html) => {
+                    this.setOption("content", html);
+                })
+                .catch((error) => {
+                    this.dispatchEvent(
+                        new CustomEvent("viewer-error", {detail: error}),
+                    );
+                    throw new Error(error);
+                });
+        } else {
+            this.dispatchEvent(
+                new CustomEvent("viewer-error", {
+                    detail: "HTMLElement or string expected",
+                }),
+            );
+            throw new Error("HTMLElement or string expected");
+        }
+
+        this.setOption("content", data);
+    }
+
+    /**
+     * Sets the plain text content by processing the input data, which can be of various types, including Blob,
+     * HTMLElement, string, or a valid URL. The method extracts and sets the raw text content into a predefined option.
+     *
+     * @param {Blob|HTMLElement|string} data - The input data to be processed. It can be a Blob object, an HTMLElement,
+     * a plain string, or a string formatted as a valid URL. The method determines
+     * the data type and processes it accordingly.
+     * @return {void} - This method does not return any value. It processes the content and updates the relevant option
+     * property.
+     */
+    setPlainText(data) {
+        const mkPreSpan = (text) => {
+            const pre = document.createElement("pre");
+            pre.innerText = text;
+            pre.setAttribute("part", "text");
+            return pre.outerHTML;
+        };
+
+        if (data instanceof Blob) {
+            blobToText(data)
+                .then((text) => {
+                    const div = document.createElement("div");
+                    div.innerHTML = text;
+                    text = div.innerText;
+
+                    this.setOption("content", mkPreSpan(text));
+                })
+                .catch((error) => {
+                    this.dispatchEvent(
+                        new CustomEvent("viewer-error", {detail: error}),
+                    );
+                    throw new Error(error);
+                });
+
+            return;
+        } else if (data instanceof HTMLElement) {
+            data = data.outerText;
+        } else if (isString(data)) {
+            const div = document.createElement("div");
+            div.innerHTML = data;
+            data = div.innerText;
+        } else if (isURL(data)) {
+            getGlobal()
+                .fetch(data)
+                .then((response) => {
+                    return response.text();
+                })
+                .then((text) => {
+                    const div = document.createElement("div");
+                    div.innerHTML = text;
+                    text = div.innerText;
+
+                    this.setOption("content", mkPreSpan(text));
+                })
+                .catch((error) => {
+                    this.dispatchEvent(
+                        new CustomEvent("viewer-error", {detail: error}),
+                    );
+                    throw new Error(error);
+                });
+        } else {
+            this.dispatchEvent(
+                new CustomEvent("viewer-error", {
+                    detail: "HTMLElement or string expected",
+                }),
+            );
+            throw new Error("HTMLElement or string expected");
+        }
+
+        this.setOption("content", mkPreSpan(data));
+    }
+
+    /**
+     *
+     * @return {Viewer}
+     */
+    [assembleMethodSymbol]() {
+        super[assembleMethodSymbol]();
+
+        initControlReferences.call(this);
+        initEventHandler.call(this);
+    }
+
+    /**
+     *
+     * @return {string}
+     */
+    static getTag() {
+        return "monster-viewer";
+    }
+
+    /**
+     * @return {CSSStyleSheet[]}
+     */
+    static getCSSStyleSheet() {
+        return [ViewerStyleSheet];
+    }
 }
 
 /**
@@ -654,12 +710,12 @@ class Viewer extends CustomElement {
  * @return {boolean}
  */
 function isURL(variable) {
-	try {
-		new URL(variable);
-		return true;
-	} catch (error) {
-		return false;
-	}
+    try {
+        new URL(variable);
+        return true;
+    } catch (error) {
+        return false;
+    }
 }
 
 /**
@@ -668,7 +724,7 @@ function isURL(variable) {
  * @return {boolean}
  */
 function isBlob(variable) {
-	return variable instanceof Blob;
+    return variable instanceof Blob;
 }
 
 /**
@@ -677,12 +733,12 @@ function isBlob(variable) {
  * @return {Promise<unknown>}
  */
 function blobToText(blob) {
-	return new Promise((resolve, reject) => {
-		const reader = new FileReader();
-		reader.onloadend = () => resolve(reader.result);
-		reader.onerror = reject;
-		reader.readAsText(blob);
-	});
+    return new Promise((resolve, reject) => {
+        const reader = new FileReader();
+        reader.onloadend = () => resolve(reader.result);
+        reader.onerror = reject;
+        reader.readAsText(blob);
+    });
 }
 
 /**
@@ -691,18 +747,122 @@ function blobToText(blob) {
  * @throws {Error} no shadow-root is defined
  */
 function initControlReferences() {
-	if (!this.shadowRoot) {
-		throw new Error("no shadow-root is defined");
-	}
+    if (!this.shadowRoot) {
+        throw new Error("no shadow-root is defined");
+    }
 
-	this[viewerElementSymbol] = this.shadowRoot.getElementById("viewer");
+    this[viewerElementSymbol] = this.shadowRoot.getElementById("viewer");
 }
 
 /**
  * @private
  */
 function initEventHandler() {
-	return this;
+    return this;
+}
+
+function getLabels() {
+    switch (getLocaleOfDocument().language) {
+        case "de": // German
+            return {
+                download: "Herunterladen",
+            };
+
+        case "es": // Spanish
+            return {
+                download: "Descargar",
+            };
+
+        case "zh": // Mandarin
+            return {
+                download: "下载",
+            };
+
+        case "hi": // Hindi
+            return {
+                download: "下载",
+            };
+
+        case "bn": // Bengali
+            return {
+                download: "ডাউনলোড",
+            };
+
+        case "pt": // Portuguese
+            return {
+                download: "Baixar",
+            };
+
+        case "ru": // Russian
+            return {
+                download: "Скачать",
+            };
+
+        case "ja": // Japanese
+            return {
+                download: "ダウンロード",
+            };
+
+        case "pa": // Western Punjabi
+            return {
+                download: "ਡਾਊਨਲੋਡ",
+            };
+
+        case "mr": // Marathi
+            return {
+                download: "डाउनलोड",
+            };
+
+        case "fr": // French
+            return {
+                download: "Télécharger",
+            };
+
+        case "it": // Italian
+            return {
+                download: "Scarica",
+            };
+
+        case "nl": // Dutch
+            return {
+                download: "Downloaden",
+            };
+
+        case "sv": // Swedish
+            return {
+                download: "Ladda ner",
+            };
+
+        case "pl": // Polish
+            return {
+                download: "Ściągnij",
+            };
+
+        case "da": // Danish
+            return {
+                download: "Lad ned",
+            };
+
+        case "fi": // Finnish
+            return {
+                download: "Lataa",
+            };
+
+        case "no": // Norwegian
+            return {
+                download: "Laste ned",
+            };
+
+        case "cs": // Czech
+            return {
+                download: "Stáhnout",
+            };
+
+        default:
+            return {
+                download: "Download",
+            };
+    }
 }
 
 /**
@@ -710,8 +870,8 @@ function initEventHandler() {
  * @return {string}
  */
 function getTemplate() {
-	// language=HTML
-	return `
+    // language=HTML
+    return `
         <div id="viewer" data-monster-role="viewer" part="viewer"
              data-monster-replace="path:content"
              data-monster-attributes="class path:classes.viewer">