Source: reactive.js

// ===========================================
// REACTIVE SYSTEM
// ===========================================
// Signals-based reactive state management with declarative binding

// ===========================================
// HTML UTILITIES
// ===========================================

/**
 * @typedef {Object} SafeHTML
 * @property {boolean} __safe - Indicates the content is safe HTML
 * @property {string} content - The HTML content
 */

let _escapeElement;

/**
 * Tagged template literal for creating safe HTML content with automatic escaping.
 * Values are automatically escaped unless they are SafeHTML objects.
 *
 * @param {TemplateStringsArray} strings - Template string parts
 * @param {...*} values - Template values to interpolate
 * @returns {SafeHTML} Safe HTML object
 * @example
 * const userInput = "<script>alert('xss')</script>";
 * const safe = html`<div>${userInput}</div>`;
 * // Results in: <div>&lt;script&gt;alert('xss')&lt;/script&gt;</div>
 */
export const html = (strings, ...values) => ({
	__safe: true,
	content: strings.reduce((acc, str, i) => {
		const v = values[i];
		if (v == null) return acc + str;
		if (v.__safe) return acc + str + v.content;
		if (!_escapeElement) _escapeElement = document.createElement("div");
		_escapeElement.textContent = String(v);
		return acc + str + _escapeElement.innerHTML;
	}, ""),
});

/**
 * Marks content as trusted HTML that should not be escaped.
 * Use with caution - only for content you control.
 *
 * @param {string} content - HTML content to mark as trusted
 * @returns {SafeHTML} Safe HTML object
 * @example
 * const icon = trusted("<svg>...</svg>");
 */
export const trusted = (content) => ({ __safe: true, content });

/**
 * Joins multiple items (strings or SafeHTML) with a separator.
 *
 * @param {Array<string|SafeHTML>} items - Items to join
 * @param {string|SafeHTML} [separator=""] - Separator to use between items
 * @returns {SafeHTML} Safe HTML object with joined content
 * @example
 * const list = join([html`<li>A</li>`, html`<li>B</li>`], "\n");
 */
export const join = (items, separator = "") => ({
	__safe: true,
	content: items
		.map((i) => (i?.__safe ? i.content : String(i)))
		.join(typeof separator === "string" ? separator : separator.content || ""),
});

// CSS-in-JS
const _styleCache = new Set();

/**
 * Creates scoped CSS with automatic class name generation and injection.
 * Supports nested selectors with & and automatic prefixing.
 *
 * @param {TemplateStringsArray} strings - Template string parts
 * @param {...*} values - Template values to interpolate
 * @returns {string} Generated CSS class name
 * @example
 * const buttonClass = css`
 *   padding: 10px;
 *   &:hover { background: blue; }
 *   .icon { margin-right: 5px; }
 * `;
 */
export const css = (strings, ...values) => {
	const content = strings.reduce(
		(acc, str, i) => acc + str + (values[i] || ""),
		"",
	);
	// Simple hash for class name
	let hash = 0;
	for (let i = 0; i < content.length; i++)
		hash = (hash << 5) - hash + content.charCodeAt(i);
	const className = `s-${(hash >>> 0).toString(36)}`;

	if (!_styleCache.has(className)) {
		_styleCache.add(className);
		// Skip style injection if document is not available (e.g., in Node.js test environment)
		if (typeof document === "undefined") {
			return className;
		}
		const style = document.createElement("style");
		// Split into rules and process each one
		// For root-level properties (not in a selector), prefix with the class
		// For & selectors, replace with the class
		// For other selectors (like .child), prefix with the class
		let processedContent = content;

		// First, handle & references
		processedContent = processedContent.replace(/&/g, `.${className}`);

		// Then wrap root-level properties (properties before the first {)
		// and prefix child selectors
		const lines = processedContent.split("\n");
		const result = [];
		let inBlock = 0;
		let currentRule = "";
		let rootProperties = "";

		for (const line of lines) {
			const trimmed = line.trim();

			// Count braces to track nesting
			const openBraces = (line.match(/{/g) || []).length;
			const closeBraces = (line.match(/}/g) || []).length;

			if (
				inBlock === 0 &&
				trimmed &&
				!trimmed.startsWith("@") &&
				!trimmed.includes("{") &&
				trimmed.includes(":") &&
				!trimmed.endsWith(",")
			) {
				// Root-level property
				rootProperties += `\t${trimmed}\n`;
			} else if (
				trimmed.startsWith(".") ||
				trimmed.startsWith("@") ||
				trimmed.startsWith(":")
			) {
				// Selector or at-rule
				if (rootProperties) {
					result.push(`.${className} {\n${rootProperties}}\n`);
					rootProperties = "";
				}
				currentRule += `${line}\n`;
			} else {
				currentRule += `${line}\n`;
			}

			inBlock += openBraces - closeBraces;

			if (inBlock === 0 && currentRule.trim()) {
				result.push(currentRule);
				currentRule = "";
			}
		}

		if (rootProperties) {
			result.push(`.${className} {\n${rootProperties}}\n`);
		}
		if (currentRule.trim()) {
			result.push(currentRule);
		}

		style.textContent = result.join("");
		document.head.appendChild(style);
	}
	return className;
};

// ===========================================
// SIGNALS
// ===========================================

/**
 * @typedef {Object} Signal
 * @property {function(): *} get - Get the current value and track dependency
 * @property {function(): *} peek - Get the current value without tracking
 * @property {function(*): void} set - Set a new value
 * @property {function(function(*): void): function(): void} subscribe - Subscribe to changes
 * @property {function(function(*): void): function(): void} subscribeInternal - Internal subscription without immediate call
 * @property {function(function(*): void): function(): void} once - Subscribe and automatically unsubscribe after first call
 * @property {function(function(*): *): void} update - Update value using a function
 * @property {*} value - Getter/setter property for the signal value
 */

/**
 * @typedef {Object} ComputedSignal
 * @extends Signal
 * @property {function(): void} dispose - Clean up subscriptions and dependencies
 */

/**
 * @typedef {Object} AsyncState
 * @property {"pending"|"resolved"|"error"} status - Current status of the async operation
 * @property {*} data - The resolved data (undefined until resolved)
 * @property {Error|null} error - Error object if status is "error"
 * @property {boolean} loading - True when status is "pending"
 */

/**
 * @typedef {Object} CancelToken
 * @property {boolean} cancelled - True if the operation was cancelled
 */

let _activeContext = null;
let _batchPending = false;
const _batchQueue = new Set();
const _batchWrappers = new WeakMap();

// Debug mode
let _debugMode = false;

/**
 * Enable or disable debug mode for reactive system logging.
 * When enabled, logs signal updates, computed recalculations, and async state changes.
 *
 * @param {boolean} enabled - Whether to enable debug mode
 * @example
 * setDebugMode(true); // Enable debug logging
 */
export const setDebugMode = (enabled) => {
	_debugMode = enabled;
};
const _debugLog = (...args) => {
	if (_debugMode) console.log("[Reactive]", ...args);
};

// Circular dependency detection for computed values
const _computeStack = [];

/**
 * Core Signals API for reactive state management.
 * Provides primitives for creating reactive values, computed values, and async computations.
 * @namespace Signals
 */
export const Signals = {
	/**
	 * Creates a reactive signal that holds a single value.
	 * Signals track dependencies and notify subscribers when the value changes.
	 *
	 * @param {*} value - Initial value
	 * @param {function(*, *): boolean} [equals=(a, b) => a === b] - Equality function to determine if value changed
	 * @param {string|null} [name=null] - Optional name for debugging
	 * @returns {Signal} Signal object with get/set/subscribe methods
	 * @example
	 * const count = Signals.create(0, undefined, "count");
	 * count.set(5);
	 * console.log(count.get()); // 5
	 * count.subscribe(val => console.log("Changed:", val));
	 */
	create(value, equals = (a, b) => a === b, name = null) {
		const subs = new Set();
		const signal = {
			get() {
				if (_activeContext) _activeContext.add(signal);
				return value;
			},
			peek() {
				return value;
			},
			set(newVal) {
				if (equals(value, newVal)) return;
				const oldVal = value;
				value = newVal;
				_debugLog(
					"Signal updated:",
					name ? `[${name}]` : "",
					oldVal,
					"->",
					newVal,
				);
				if (_batchPending) {
					for (const fn of subs) {
						// Get or create wrapper function for this subscriber that reads latest value
						let wrapper = _batchWrappers.get(fn);
						if (!wrapper) {
							wrapper = () => fn(signal.get());
							_batchWrappers.set(fn, wrapper);
						}
						_batchQueue.add(wrapper);
					}
				} else {
					for (const fn of [...subs]) fn(value);
				}
			},
			subscribe(fn) {
				subs.add(fn);
				fn(value);
				return () => subs.delete(fn);
			},
			subscribeInternal(fn) {
				subs.add(fn);
				return () => subs.delete(fn);
			},
			once(fn) {
				let called = false;
				let unsub;
				const wrapper = (val) => {
					if (!called) {
						called = true;
						fn(val);
						if (unsub) unsub();
					}
				};
				unsub = signal.subscribe(wrapper);
				return unsub;
			},
			update(fn) {
				signal.set(fn(value));
			},
			toString() {
				return name ? `Signal(${name})` : "Signal";
			},
			get value() {
				return signal.get();
			},
			set value(newVal) {
				signal.set(newVal);
			},
		};
		if (name) signal._name = name;
		return signal;
	},

	/**
	 * Creates a computed signal that automatically recalculates when dependencies change.
	 * Dependencies are tracked automatically when accessed inside the computation function.
	 *
	 * @param {function(): *} fn - Computation function that returns the computed value
	 * @param {string|null} [name=null] - Optional name for debugging
	 * @returns {ComputedSignal} Computed signal with dispose method
	 * @throws {Error} If circular dependency is detected
	 * @example
	 * const firstName = Signals.create("John");
	 * const lastName = Signals.create("Doe");
	 * const fullName = Signals.computed(() => `${firstName.get()} ${lastName.get()}`);
	 * console.log(fullName.get()); // "John Doe"
	 */
	computed(fn, name = null) {
		const result = Signals.create(undefined, undefined, name);
		let deps = new Set();
		const unsubs = [];
		let computing = false;

		const run = () => {
			if (computing) return;

			// Circular dependency detection
			if (_computeStack.includes(result)) {
				const cycle = [..._computeStack, result]
					.map((s) => s._name || "anonymous")
					.join(" -> ");
				throw new Error(
					`Circular dependency detected in computed signal: ${cycle}`,
				);
			}

			computing = true;
			_computeStack.push(result);
			const prev = _activeContext;
			_activeContext = new Set();
			try {
				const value = fn();
				_debugLog("Computed updated:", name ? `[${name}]` : "", value);
				result.set(value);
				const newDeps = _activeContext;
				// Remove unused
				[...deps]
					.filter((d) => !newDeps.has(d))
					.forEach((d) => {
						const idx = unsubs.findIndex((u) => u.dep === d);
						if (idx > -1) unsubs.splice(idx, 1)[0].unsub();
					});
				// Add new
				[...newDeps]
					.filter((d) => !deps.has(d))
					.forEach((d) => {
						unsubs.push({ dep: d, unsub: d.subscribeInternal(scheduler) });
					});
				deps = newDeps;
			} catch (error) {
				console.error(
					"[Reactive] Computed error:",
					name ? `[${name}]` : "",
					error,
				);
				throw error;
			} finally {
				_computeStack.pop();
				_activeContext = prev;
				computing = false;
			}
		};

		const scheduler = () => {
			if (_batchPending) _batchQueue.add(run);
			else run();
		};

		run();
		result.dispose = () => {
			for (const u of unsubs) u.unsub();
			unsubs.length = 0;
			deps.clear();
		};
		return result;
	},

	/**
	 * Creates a computed signal for async operations that tracks loading/error/data states.
	 * Automatically cancels previous execution when dependencies change.
	 *
	 * @param {function(CancelToken): Promise<*>} fn - Async computation function
	 * @param {string|null} [name=null] - Optional name for debugging
	 * @returns {ComputedSignal<AsyncState>} Signal containing {status, data, error, loading}
	 * @example
	 * const userId = Signals.create(1);
	 * const userData = Signals.computedAsync(async (cancel) => {
	 *   const response = await fetch(`/api/users/${userId.get()}`);
	 *   if (cancel.cancelled) return;
	 *   return response.json();
	 * });
	 * // userData.get() returns {status: "pending", data: undefined, error: null, loading: true}
	 */
	computedAsync(fn, name = null) {
		const result = Signals.create(
			{ status: "pending", data: undefined, error: null, loading: true },
			undefined,
			name,
		);
		let deps = new Set();
		const unsubs = [];
		let currentCancel = null;

		const run = async () => {
			// Cancel previous execution if still running
			if (currentCancel) {
				currentCancel.cancelled = true;
			}

			// Create cancellation token for this execution
			const cancelToken = { cancelled: false };
			currentCancel = cancelToken;

			// Update state to pending
			result.set({
				status: "pending",
				data: result.peek().data,
				error: null,
				loading: true,
			});

			// Create a dedicated context for this async execution
			const myContext = new Set();
			const prev = _activeContext;
			_activeContext = myContext;

			try {
				const value = await fn(cancelToken);

				// Check if this execution was cancelled
				if (cancelToken.cancelled) {
					_debugLog("Async computed cancelled:", name ? `[${name}]` : "");
					_activeContext = prev;
					return;
				}

				_debugLog("Async computed resolved:", name ? `[${name}]` : "", value);

				result.set({
					status: "resolved",
					data: value,
					error: null,
					loading: false,
				});

				// Use our dedicated context (myContext) instead of whatever _activeContext is now
				const newDeps = myContext;
				_activeContext = prev; // Restore previous context

				// Remove unused
				[...deps]
					.filter((d) => !newDeps.has(d))
					.forEach((d) => {
						const idx = unsubs.findIndex((u) => u.dep === d);
						if (idx > -1) unsubs.splice(idx, 1)[0].unsub();
					});
				// Add new
				[...newDeps]
					.filter((d) => !deps.has(d))
					.forEach((d) => {
						unsubs.push({ dep: d, unsub: d.subscribeInternal(scheduler) });
					});
				deps = newDeps;
			} catch (error) {
				// Check if this execution was cancelled
				if (cancelToken.cancelled) {
					_activeContext = prev;
					return;
				}

				_debugLog("Async computed error:", name ? `[${name}]` : "", error);

				result.set({
					status: "error",
					data: result.peek().data,
					error,
					loading: false,
				});

				_activeContext = prev; // Restore context after error handling
			} finally {
				if (currentCancel === cancelToken) {
					currentCancel = null;
				}
			}
		};

		const scheduler = () => {
			if (_batchPending) _batchQueue.add(run);
			else run();
		};

		run();
		result.dispose = () => {
			if (currentCancel) {
				currentCancel.cancelled = true;
			}
			for (const u of unsubs) u.unsub();
			unsubs.length = 0;
			deps.clear();
		};
		return result;
	},

	/**
	 * Batches multiple signal updates to prevent redundant recalculations.
	 * All updates within the function are queued and executed once at the end.
	 *
	 * @param {function(): *} fn - Function containing signal updates
	 * @returns {*} Return value of the function
	 * @example
	 * Signals.effect(() => {
	 *   count.set(1);
	 *   count.set(2);
	 *   count.set(3);
	 * }); // Subscribers only notified once with value 3
	 */
	effect(fn) {
		_batchPending = true;
		try {
			return fn();
		} finally {
			_batchPending = false;
			const q = new Set(_batchQueue);
			_batchQueue.clear();
			for (const fn of q) fn();
		}
	},

	/**
	 * Alias for effect(). Batches multiple signal updates together.
	 *
	 * @param {function(): *} fn - Function containing signal updates
	 * @returns {*} Return value of the function
	 * @see {@link Signals.effect}
	 */
	batch(fn) {
		return Signals.effect(fn);
	},
};

// ===========================================
// REACTIVE UTILITIES
// ===========================================

/**
 * Reactive utilities for DOM binding and component management.
 * Provides methods to bind signals to DOM elements and manage reactive components.
 * @namespace Reactive
 */
export const Reactive = {
	/**
	 * Mounts a reactive function to an element, updating its innerHTML.
	 *
	 * @param {HTMLElement} el - Target element
	 * @param {function(): (string|SafeHTML)} fn - Function that returns content
	 * @returns {{update: function(): void}} Object with update method
	 */
	mount(el, fn) {
		const update = () => {
			const res = fn();
			el.innerHTML = res.__safe ? res.content : String(res);
		};
		update();
		return { update };
	},

	/**
	 * Binds a signal to an element's innerHTML through a transformation function.
	 *
	 * @param {HTMLElement} el - Target element
	 * @param {Signal} sig - Signal to bind
	 * @param {function(*): (string|SafeHTML)} fn - Transform function
	 * @returns {function(): void} Unsubscribe function
	 * @example
	 * const count = Signals.create(0);
	 * Reactive.bind(div, count, val => html`Count: ${val}`);
	 */
	bind(el, sig, fn) {
		return sig.subscribe((val) => {
			const v = val === undefined ? sig.get() : val;
			const res = fn(v);
			el.innerHTML = res.__safe ? res.content : String(res);
		});
	},

	/**
	 * Binds a signal to an element's attribute.
	 *
	 * @param {HTMLElement} el - Target element
	 * @param {string} attr - Attribute name
	 * @param {Signal} sig - Signal to bind
	 * @returns {function(): void} Unsubscribe function
	 * @example
	 * const url = Signals.create("/page");
	 * Reactive.bindAttr(link, "href", url);
	 */
	bindAttr: (el, attr, sig) =>
		sig.subscribe((val) =>
			el.setAttribute(attr, val === undefined ? sig.get() : val),
		),

	/**
	 * Binds a signal to an element's textContent.
	 *
	 * @param {HTMLElement} el - Target element
	 * @param {Signal} sig - Signal to bind
	 * @returns {function(): void} Unsubscribe function
	 * @example
	 * const message = Signals.create("Hello");
	 * Reactive.bindText(span, message);
	 */
	bindText: (el, sig) =>
		sig.subscribe((val) => {
			el.textContent = val === undefined ? sig.get() : val;
		}),

	/**
	 * Binds a signal to a boolean attribute (present/absent).
	 *
	 * @param {HTMLElement} el - Target element
	 * @param {string} attr - Attribute name (e.g., "disabled", "hidden")
	 * @param {Signal<boolean>} sig - Signal to bind
	 * @returns {function(): void} Unsubscribe function
	 * @example
	 * const isDisabled = Signals.create(false);
	 * Reactive.bindBoolAttr(button, "disabled", isDisabled);
	 */
	bindBoolAttr: (el, attr, sig) =>
		sig.subscribe((val) =>
			(val === undefined ? sig.get() : val)
				? el.setAttribute(attr, "")
				: el.removeAttribute(attr),
		),

	/**
	 * Binds a signal to toggle a CSS class on an element.
	 *
	 * @param {HTMLElement} el - Target element
	 * @param {string} cls - CSS class name
	 * @param {Signal<boolean>} sig - Signal to bind
	 * @returns {function(): void} Unsubscribe function
	 * @example
	 * const isActive = Signals.create(true);
	 * Reactive.bindClass(div, "active", isActive);
	 */
	bindClass: (el, cls, sig) =>
		sig.subscribe((val) =>
			el.classList.toggle(cls, val === undefined ? sig.get() : val),
		),

	/**
	 * Binds a signal to a CSS style property.
	 *
	 * @param {HTMLElement} el - Target element
	 * @param {string} prop - CSS property name (camelCase)
	 * @param {Signal} sig - Signal to bind
	 * @returns {function(): void} Unsubscribe function
	 * @example
	 * const bgColor = Signals.create("red");
	 * Reactive.bindStyle(div, "backgroundColor", bgColor);
	 */
	bindStyle: (el, prop, sig) =>
		sig.subscribe((val) => {
			el.style[prop] = val === undefined ? sig.get() : val;
		}),

	/**
	 * Binds multiple signals to an element using a combine function.
	 * Creates a computed signal that tracks all input signals.
	 *
	 * @param {HTMLElement} el - Target element
	 * @param {Array<Signal>} signals - Array of signals to combine
	 * @param {function(Array<*>): (string|SafeHTML)} fn - Function that combines signal values
	 * @returns {function(): void} Unsubscribe function
	 * @throws {Error} If signals is not an array
	 * @example
	 * const first = Signals.create("John");
	 * const last = Signals.create("Doe");
	 * Reactive.bindMultiple(div, [first, last], ([f, l]) => `${f} ${l}`);
	 */
	bindMultiple(el, signals, fn) {
		if (!Array.isArray(signals)) {
			throw new Error("bindMultiple expects an array of signals");
		}
		const computed = Signals.computed(() => {
			return fn(signals.map((s) => s.get()));
		});
		const unsub = computed.subscribe((val) => {
			const res = val === undefined ? computed.get() : val;
			el.textContent = res?.__safe ? res.content : String(res);
		});
		return () => {
			computed.dispose();
			unsub();
		};
	},

	/**
	 * Scans a DOM tree for data attributes and binds them to signals in the scope.
	 * Supports: data-text, data-html, data-visible, data-if, data-model,
	 * data-class-*, data-attr-*, data-bool-*, data-on-*
	 *
	 * @param {HTMLElement} root - Root element to scan
	 * @param {Object} scope - Object containing signals to bind
	 * @returns {function(): void} Cleanup function to unbind all
	 * @example
	 * const scope = { message: Signals.create("Hello") };
	 * // <div data-text="message"></div>
	 * const cleanup = Reactive.scan(document.body, scope);
	 */
	scan(root, scope) {
		const unsubs = [];
		const resolve = (path) => path.split(".").reduce((o, k) => o?.[k], scope);

		const handlers = {
			"data-text": (el, val) => Reactive.bindText(el, val),
			"data-html": (el, val) => {
				let cleanups = [];
				const unsub = val.subscribe((v) => {
					for (const f of cleanups) f?.();
					cleanups = [];
					const res = v === undefined ? val.get() : v;
					el.innerHTML = res?.__safe ? res.content : String(res);

					// Scan children only to avoid infinite recursion on self
					for (const child of el.children) {
						cleanups.push(Reactive.scan(child, scope));
					}
				});
				return () => {
					for (const f of cleanups) f?.();
					unsub();
				};
			},
			"data-visible": (el, val) =>
				val.subscribe((v) => {
					el.style.display = (v === undefined ? val.get() : v) ? "" : "none";
				}),
			"data-if": (el, val) => {
				const placeholder = document.createComment("if");
				const currentEl = el;
				el.parentNode?.insertBefore(placeholder, el);
				return val.subscribe((v) => {
					const show = v === undefined ? val.get() : v;
					if (show && !currentEl.parentNode) {
						placeholder.parentNode?.insertBefore(
							currentEl,
							placeholder.nextSibling,
						);
					} else if (!show && currentEl.parentNode) {
						currentEl.parentNode.removeChild(currentEl);
					}
				});
			},
			"data-model": (el, val) => {
				if (!val?.set) return;
				unsubs.push(
					val.subscribe((v) => {
						const value = v === undefined ? val.get() : v;
						if (el.value !== value) el.value = value || "";
					}),
				);
				const h = () => val.set(el.value);
				el.addEventListener("input", h);
				return () => el.removeEventListener("input", h);
			},
		};

		const walk = document.createTreeWalker(root, 1);
		let node = walk.currentNode;
		while (node) {
			const el = node;
			if (el.nodeType === 1) {
				for (const { name, value } of [...el.attributes]) {
					const val = resolve(value);
					if (handlers[name]) {
						const u = handlers[name](el, val);
						if (u) unsubs.push(u);
					} else if (name.startsWith("data-class-")) {
						unsubs.push(Reactive.bindClass(el, name.slice(11), val));
					} else if (name.startsWith("data-attr-")) {
						unsubs.push(Reactive.bindAttr(el, name.slice(10), val));
					} else if (name.startsWith("data-bool-")) {
						unsubs.push(Reactive.bindBoolAttr(el, name.slice(10), val));
					} else if (name.startsWith("data-on-")) {
						if (typeof val === "function") {
							const h = (e) => Signals.batch(() => val.call(scope, e));
							const evt = name.slice(8);
							el.addEventListener(evt, h);
							unsubs.push(() => el.removeEventListener(evt, h));
						}
					}
				}
			}
			node = walk.nextNode();
		}
		return () => {
			for (const f of unsubs) f?.();
		};
	},

	/**
	 * Creates a component context for managing subscriptions and computed signals.
	 * Provides methods for tracking cleanup functions and creating scoped reactivity.
	 *
	 * @returns {Object} Component context with track, computed, scan, bind methods
	 * @example
	 * const ctx = Reactive.createComponent();
	 * const count = Signals.create(0);
	 * ctx.bindText(element, count);
	 * ctx.cleanup(); // Cleans up all bindings
	 */
	createComponent() {
		const unsubs = [],
			computed = [];
		const c = {
			track: (u) => {
				if (u) unsubs.push(u);
				return u;
			},
			computed: (fn, name) => {
				const s = Signals.computed(fn, name);
				computed.push(s);
				return s;
			},
			computedAsync: (fn, name) => {
				const s = Signals.computedAsync(fn, name);
				computed.push(s); // Track for disposal
				return s;
			},
			scan: (r, s) => c.track(Reactive.scan(r, s)),
			cleanup: () => {
				for (const f of unsubs) {
					typeof f === "function" ? f() : f?.unsubscribe?.();
				}
				for (const s of computed) s.dispose();
				unsubs.length = computed.length = 0;
			},
		};
		for (const m of [
			"bind",
			"bindAttr",
			"bindBoolAttr",
			"bindClass",
			"bindText",
			"bindStyle",
			"bindMultiple",
		]) {
			c[m] = (...a) => c.track(Reactive[m](...a));
		}
		return c;
	},

	/**
	 * Base class for creating reactive components with lifecycle methods.
	 * Provides automatic state management, DOM binding, and cleanup.
	 *
	 * Lifecycle methods (override in subclass):
	 * - state(): Returns initial state object (converted to signals)
	 * - init(): Called after state initialization, before rendering (optional)
	 * - template(): Returns html tagged template for the component (required)
	 * - styles(): Returns css class name for styling (optional)
	 * - mount(): Called after component is mounted to DOM (optional)
	 * - onCleanup(): Called during cleanup (optional)
	 *
	 * @class
	 * @example
	 * class Counter extends Reactive.Component {
	 *   state() {
	 *     return { count: 0 };
	 *   }
	 *   template() {
	 *     return html`<button data-on-click="increment">${this.count}</button>`;
	 *   }
	 *   increment() {
	 *     this.count.set(this.count.get() + 1);
	 *   }
	 * }
	 */
	Component: class {
		/**
		 * Creates a new component instance.
		 * Initializes internal reactive context and binding methods.
		 */
		constructor() {
			this._c = Reactive.createComponent();
			this.refs = {};
			for (const m of [
				"bind",
				"bindAttr",
				"bindBoolAttr",
				"bindClass",
				"bindText",
				"bindStyle",
				"bindMultiple",
				"track",
			]) {
				this[m] = (...a) => this._c[m](...a);
			}
		}

		/**
		 * Creates a signal within the component.
		 *
		 * @param {*} v - Initial value
		 * @param {string} [name] - Optional name for debugging
		 * @returns {Signal} Signal object
		 */
		signal(v, name) {
			return Signals.create(v, undefined, name);
		}

		/**
		 * Attaches an event listener that's automatically cleaned up.
		 * Handler is wrapped in a batch for efficient updates.
		 *
		 * @param {EventTarget} target - Element to attach listener to
		 * @param {string} event - Event name
		 * @param {function(Event): void} handler - Event handler
		 * @param {Object} [options] - Event listener options
		 * @returns {function(Event): void} Bound handler function
		 */
		on(target, event, handler, options) {
			const boundHandler = (e) => this.batch(() => handler.call(this, e));
			target.addEventListener(event, boundHandler, options);
			this.track(() =>
				target.removeEventListener(event, boundHandler, options),
			);
			return boundHandler;
		}

		/**
		 * Creates a computed signal within the component.
		 * Automatically disposed when component is cleaned up.
		 *
		 * @param {function(): *} fn - Computation function
		 * @param {string} [name] - Optional name for debugging
		 * @returns {ComputedSignal} Computed signal
		 */
		computed(fn, name) {
			return this._c.computed(fn, name);
		}

		/**
		 * Creates an async computed signal within the component.
		 * Automatically disposed when component is cleaned up.
		 *
		 * @param {function(CancelToken): Promise<*>} fn - Async computation function
		 * @param {string} [name] - Optional name for debugging
		 * @returns {ComputedSignal<AsyncState>} Async computed signal
		 */
		computedAsync(fn, name) {
			const asyncComputed = Signals.computedAsync(fn, name);
			this.track(() => asyncComputed.dispose());
			return asyncComputed;
		}

		/**
		 * Creates a side effect that runs when dependencies change.
		 *
		 * @param {function(): void} fn - Effect function
		 * @returns {ComputedSignal} Computed signal (for cleanup)
		 */
		effect(fn) {
			return this.computed(() => {
				fn();
				return undefined;
			});
		}

		/**
		 * Batches multiple signal updates together.
		 *
		 * @param {function(): *} fn - Function containing updates
		 * @returns {*} Return value of the function
		 */
		batch(fn) {
			return Signals.batch(fn);
		}

		/**
		 * Initializes component state by processing the state() method.
		 * Called automatically before rendering. Can be overridden.
		 * Calls init() hook if defined.
		 */
		initState() {
			if (this.state) this._proc(this.state(), this);
			if (this.init) this.init();
		}

		/**
		 * Internal method to process state object, converting values to signals.
		 * Functions become computed signals, objects with .get() are preserved.
		 *
		 * @private
		 * @param {Object} obj - State object to process
		 * @param {Object} tgt - Target object to assign signals to
		 */
		_proc(obj, tgt) {
			Object.entries(obj).forEach(([k, v]) => {
				if (typeof v === "function") tgt[k] = this.computed(v);
				else if (v?.get) tgt[k] = v;
				else if (
					v &&
					typeof v === "object" &&
					!Array.isArray(v) &&
					Object.values(v).some((x) => x?.get)
				)
					tgt[k] = v;
				else tgt[k] = this.signal(v);
			});
		}

		/**
		 * Scans a DOM tree for data attributes and collects refs.
		 * Updates this.refs with elements marked with data-ref.
		 *
		 * @param {HTMLElement} r - Root element to scan
		 * @returns {function(): void} Cleanup function
		 */
		scan(r) {
			// Collect refs - check the root element first, then descendants
			// querySelectorAll only finds descendants, not the element itself
			if (r.hasAttribute?.("data-ref")) {
				const refName = r.getAttribute("data-ref");
				if (refName) this.refs[refName] = r;
			}
			r.querySelectorAll("[data-ref]").forEach((el) => {
				const refName = el.getAttribute("data-ref");
				if (refName) this.refs[refName] = el;
			});
			return this._c.scan(r, this);
		}

		/**
		 * Renders the component by calling template() and applying styles().
		 * Override template() and optionally styles() in your component.
		 *
		 * @returns {HTMLElement} Rendered DOM element
		 * @throws {Error} If template doesn't return valid HTML
		 */
		render() {
			try {
				const t = document.createElement("div");
				const templateResult = this.template();
				if (!templateResult || !templateResult.content) {
					throw new Error("Template must return html`` tagged template");
				}
				t.innerHTML = templateResult.content;
				const el = t.firstElementChild;
				if (!el) {
					throw new Error("Template must return a single root element");
				}
				if (this.styles) {
					el.classList.add(this.styles());
				}
				this.scan(el);
				return el;
			} catch (error) {
				console.error(
					"[Reactive] Component render error:",
					error,
					this.constructor.name,
				);
				console.error("Stack trace:", error.stack);
				const errorEl = document.createElement("div");
				errorEl.className = "component-error";
				errorEl.style.cssText =
					"padding: 20px; margin: 20px; border: 2px solid #ff6b6b; border-radius: 8px; background: #ffe0e0; color: #c92a2a; font-family: monospace;";
				errorEl.innerHTML = `
					<h3 style="margin-top: 0;">⚠️ Failed to render component</h3>
					<p><strong>Component:</strong> ${this.constructor.name}</p>
					<p><strong>Error:</strong> ${error.message}</p>
					<p><strong>Type:</strong> ${error.name}</p>
					<details><summary>Stack Trace</summary><pre style="overflow: auto;">${error.stack}</pre></details>
				`;
				return errorEl;
			}
		}
		mountTo(containerId) {
			const container = document.getElementById(containerId);
			if (!container) {
				console.error(
					`[Reactive] Container #${containerId} not found for component ${this.constructor.name}`,
				);
				return null;
			}
			this.initState();
			const element = this.render();
			container.innerHTML = "";
			container.appendChild(element);
			if (this.mount) this.mount();
			return element;
		}

		/**
		 * Appends the component to a container without clearing it.
		 * Calls lifecycle: state() → init() → render() → mount()
		 *
		 * @param {string} containerId - ID of the container element or "body"
		 * @returns {HTMLElement|null} Rendered element or null if container not found
		 */
		appendTo(containerId) {
			// Special case for body element (no ID)
			const container =
				containerId === "body"
					? document.body
					: document.getElementById(containerId);

			if (!container) {
				console.warn(`Container #${containerId} not found`);
				return null;
			}
			this.initState();
			const element = this.render();
			container.appendChild(element);
			if (this.mount) this.mount();
			return element;
		}

		/**
		 * Cleans up the component by calling onCleanup() hook and disposing all reactive bindings.
		 * Automatically called when component is destroyed.
		 */
		cleanup() {
			if (this.onCleanup) this.onCleanup();
			this._c.cleanup();
			this.refs = {};
		}
	},
};