spaceLimited = (
	itemsAndGroups,
	max,
	filteredChildrenLineReserved = false
) => {
	if (max < 1) {
		return /** @type {Children<T>} */ ({
			children: undefined,
			filteredChildren: getTotalItems(itemsAndGroups)
		});
	}
	/** @type {Children<T>[] | undefined} */
	let children;
	/** @type {number | undefined} */
	let filteredChildren;
	// This are the groups, which take 1+ lines each
	/** @type {Children<T>[] | undefined} */
	const groups = [];
	// The sizes of the groups are stored in groupSizes
	/** @type {number[]} */
	const groupSizes = [];
	// This are the items, which take 1 line each
	const items = [];
	// The total of group sizes
	let groupsSize = 0;

	for (const itemOrGroup of itemsAndGroups) {
		// is item
		if (!itemOrGroup.children && !itemOrGroup.filteredChildren) {
			items.push(itemOrGroup);
		} else {
			groups.push(itemOrGroup);
			const size = getItemSize(itemOrGroup);
			groupSizes.push(size);
			groupsSize += size;
		}
	}

	if (groupsSize + items.length <= max) {
		// The total size in the current state fits into the max
		// keep all
		children = groups.length > 0 ? groups.concat(items) : items;
	} else if (groups.length === 0) {
		// slice items to max
		// inner space marks that lines for filteredChildren already reserved
		const limit = max - (filteredChildrenLineReserved ? 0 : 1);
		filteredChildren = items.length - limit;
		items.length = limit;
		children = items;
	} else {
		// limit is the size when all groups are collapsed
		const limit =
			groups.length +
			(filteredChildrenLineReserved || items.length === 0 ? 0 : 1);
		if (limit < max) {
			// calculate how much we are over the size limit
			// this allows to approach the limit faster
			let oversize;
			// If each group would take 1 line the total would be below the maximum
			// collapse some groups, keep items
			while (
				(oversize =
					groupsSize +
					items.length +
					(filteredChildren && !filteredChildrenLineReserved ? 1 : 0) -
					max) > 0
			) {
				// Find the maximum group and process only this one
				const maxGroupSize = Math.max(...groupSizes);
				if (maxGroupSize < items.length) {
					filteredChildren = items.length;
					items.length = 0;
					continue;
				}
				for (let i = 0; i < groups.length; i++) {
					if (groupSizes[i] === maxGroupSize) {
						const group = groups[i];
						// run this algorithm recursively and limit the size of the children to
						// current size - oversize / number of groups
						// So it should always end up being smaller
						const headerSize = group.filteredChildren ? 2 : 1;
						const limited = spaceLimited(
							/** @type {Children<T>[]} */ (group.children),
							maxGroupSize -
								// we should use ceil to always feet in max
								Math.ceil(oversize / groups.length) -
								// we substitute size of group head
								headerSize,
							headerSize === 2
						);
						groups[i] = {
							...group,
							children: limited.children,
							filteredChildren: limited.filteredChildren
								? (group.filteredChildren || 0) + limited.filteredChildren
								: group.filteredChildren
						};
						const newSize = getItemSize(groups[i]);
						groupsSize -= maxGroupSize - newSize;
						groupSizes[i] = newSize;
						break;
					}
				}
			}
			children = groups.concat(items);
		} else if (limit === max) {
			// If we have only enough space to show one line per group and one line for the filtered items
			// collapse all groups and items
			children = collapse(groups);
			filteredChildren = items.length;
		} else {
			// If we have no space
			// collapse complete group
			filteredChildren = getTotalItems(itemsAndGroups);
		}
	}

	return /** @type {Children<T>} */ ({ children, filteredChildren });
}
@template T
@param {Children<T>[]} itemsAndGroups item and groups
@param {number} max max
@param {boolean=} filteredChildrenLineReserved filtered children line reserved
@returns {Children<T>} result