diff --git a/packages/docusaurus-plugin-ideal-image/package.json b/packages/docusaurus-plugin-ideal-image/package.json
index b67f2377bce0..8e6f93cd412a 100644
--- a/packages/docusaurus-plugin-ideal-image/package.json
+++ b/packages/docusaurus-plugin-ideal-image/package.json
@@ -26,7 +26,6 @@
"@docusaurus/theme-translations": "3.7.0",
"@docusaurus/types": "3.7.0",
"@docusaurus/utils-validation": "3.7.0",
- "react-waypoint": "^10.3.0",
"sharp": "^0.32.3",
"tslib": "^2.6.0",
"webpack": "^5.88.1"
diff --git a/packages/docusaurus-plugin-ideal-image/src/theme/IdealImageLegacy/components/IdealImage/index.js b/packages/docusaurus-plugin-ideal-image/src/theme/IdealImageLegacy/components/IdealImage/index.js
index 4dfe4a68d9eb..5bf57e0535e2 100644
--- a/packages/docusaurus-plugin-ideal-image/src/theme/IdealImageLegacy/components/IdealImage/index.js
+++ b/packages/docusaurus-plugin-ideal-image/src/theme/IdealImageLegacy/components/IdealImage/index.js
@@ -1,6 +1,5 @@
import React, {Component} from 'react';
-// import PropTypes from 'prop-types'
-import {Waypoint} from 'react-waypoint';
+import {Waypoint} from './waypoint';
import Media from '../Media';
import {icons, loadStates} from '../constants';
import {xhrLoader, imageLoader, timeout, combineCancel} from '../loaders';
diff --git a/packages/docusaurus-plugin-ideal-image/src/theme/IdealImageLegacy/components/IdealImage/waypoint.tsx b/packages/docusaurus-plugin-ideal-image/src/theme/IdealImageLegacy/components/IdealImage/waypoint.tsx
new file mode 100644
index 000000000000..54bd554b3f30
--- /dev/null
+++ b/packages/docusaurus-plugin-ideal-image/src/theme/IdealImageLegacy/components/IdealImage/waypoint.tsx
@@ -0,0 +1,239 @@
+/*
+This is a slimmed down copy of https://github.com/civiccc/react-waypoint
+The MIT License (MIT)
+Copyright (c) 2015 Brigade
+ */
+
+import React, {createRef, ReactNode} from 'react';
+
+type ScrollContainer = Window | HTMLElement;
+
+function addEventListener(
+ element: ScrollContainer,
+ type: 'scroll' | 'resize',
+ listener: () => void,
+ options: AddEventListenerOptions,
+) {
+ element.addEventListener(type, listener, options);
+ return () => element.removeEventListener(type, listener, options);
+}
+
+type Position = 'above' | 'inside' | 'below' | 'invisible';
+
+type Props = {
+ topOffset: number;
+ bottomOffset: number;
+ onEnter: () => void;
+ onLeave: () => void;
+ children: ReactNode;
+};
+
+export function Waypoint(props: Props) {
+ return typeof window !== 'undefined' ? (
+ {props.children}
+ ) : (
+ props.children
+ );
+}
+
+// TODO maybe replace this with IntersectionObserver later?
+// IntersectionObserver doesn't support the "fast scroll" thing
+// but it's probably not a big deal
+class WaypointClient extends React.Component {
+ static defaultProps = {
+ topOffset: 0,
+ bottomOffset: 0,
+ onEnter() {},
+ onLeave() {},
+ };
+
+ scrollableAncestor?: ScrollContainer;
+ previousPosition: Position | null = null;
+ unsubscribe?: () => void;
+
+ innerRef = createRef();
+
+ override componentDidMount() {
+ this.scrollableAncestor = findScrollableAncestor(this.innerRef.current!);
+
+ const unsubscribeScroll = addEventListener(
+ this.scrollableAncestor!,
+ 'scroll',
+ this._handleScroll,
+ {passive: true},
+ );
+
+ const unsubscribeResize = addEventListener(
+ window,
+ 'resize',
+ this._handleScroll,
+ {passive: true},
+ );
+
+ this.unsubscribe = () => {
+ unsubscribeScroll();
+ unsubscribeResize();
+ };
+
+ this._handleScroll();
+ }
+
+ override componentDidUpdate() {
+ this._handleScroll();
+ }
+
+ override componentWillUnmount() {
+ this.unsubscribe?.();
+ }
+
+ _handleScroll = () => {
+ const node = this.innerRef.current;
+ const {topOffset, bottomOffset, onEnter, onLeave} = this.props;
+
+ const bounds = getBounds({
+ node: node!,
+ scrollableAncestor: this.scrollableAncestor!,
+ topOffset,
+ bottomOffset,
+ });
+
+ const currentPosition = getCurrentPosition(bounds);
+ const previousPosition = this.previousPosition;
+ this.previousPosition = currentPosition;
+
+ if (previousPosition === currentPosition) {
+ return;
+ }
+
+ if (currentPosition === 'inside') {
+ onEnter();
+ } else if (previousPosition === 'inside') {
+ onLeave();
+ }
+
+ const isRapidScrollDown =
+ previousPosition === 'below' && currentPosition === 'above';
+ const isRapidScrollUp =
+ previousPosition === 'above' && currentPosition === 'below';
+ if (isRapidScrollDown || isRapidScrollUp) {
+ onEnter();
+ onLeave();
+ }
+ };
+
+ override render() {
+ // @ts-expect-error: fix this implicit API
+ return React.cloneElement(this.props.children, {innerRef: this.innerRef});
+ }
+}
+
+/**
+ * Traverses up the DOM to find an ancestor container which has an overflow
+ * style that allows for scrolling.
+ *
+ * @return {Object} the closest ancestor element with an overflow style that
+ * allows for scrolling. If none is found, the `window` object is returned
+ * as a fallback.
+ */
+function findScrollableAncestor(inputNode: HTMLElement): ScrollContainer {
+ let node: HTMLElement = inputNode;
+
+ while (node.parentNode) {
+ // @ts-expect-error: it's fine
+ node = node.parentNode!;
+
+ if (node === document.body) {
+ // We've reached all the way to the root node.
+ return window;
+ }
+
+ const style = window.getComputedStyle(node);
+ const overflow =
+ style.getPropertyValue('overflow-y') ||
+ style.getPropertyValue('overflow');
+
+ if (
+ overflow === 'auto' ||
+ overflow === 'scroll' ||
+ overflow === 'overlay'
+ ) {
+ return node;
+ }
+ }
+
+ // A scrollable ancestor element was not found, which means that we need to
+ // do stuff on window.
+ return window;
+}
+
+type Bounds = {
+ top: number;
+ bottom: number;
+ viewportTop: number;
+ viewportBottom: number;
+};
+
+function getBounds({
+ node,
+ scrollableAncestor,
+ topOffset,
+ bottomOffset,
+}: {
+ node: Element;
+ scrollableAncestor: ScrollContainer;
+ topOffset: number;
+ bottomOffset: number;
+}): Bounds {
+ const {top, bottom} = node.getBoundingClientRect();
+
+ let contextHeight;
+ let contextScrollTop;
+ if (scrollableAncestor === window) {
+ contextHeight = window.innerHeight;
+ contextScrollTop = 0;
+ } else {
+ const ancestorElement = scrollableAncestor as HTMLElement;
+ contextHeight = ancestorElement.offsetHeight;
+ contextScrollTop = ancestorElement.getBoundingClientRect().top;
+ }
+
+ const contextBottom = contextScrollTop + contextHeight;
+
+ return {
+ top,
+ bottom,
+ viewportTop: contextScrollTop + topOffset,
+ viewportBottom: contextBottom - bottomOffset,
+ };
+}
+
+function getCurrentPosition(bounds: Bounds): Position {
+ if (bounds.viewportBottom - bounds.viewportTop === 0) {
+ return 'invisible';
+ }
+ // top is within the viewport
+ if (bounds.viewportTop <= bounds.top && bounds.top <= bounds.viewportBottom) {
+ return 'inside';
+ }
+ // bottom is within the viewport
+ if (
+ bounds.viewportTop <= bounds.bottom &&
+ bounds.bottom <= bounds.viewportBottom
+ ) {
+ return 'inside';
+ }
+ // top is above the viewport and bottom is below the viewport
+ if (
+ bounds.top <= bounds.viewportTop &&
+ bounds.viewportBottom <= bounds.bottom
+ ) {
+ return 'inside';
+ }
+ if (bounds.viewportBottom < bounds.top) {
+ return 'below';
+ }
+ if (bounds.top < bounds.viewportTop) {
+ return 'above';
+ }
+ return 'invisible';
+}
diff --git a/yarn.lock b/yarn.lock
index 781ab1ef61c8..cf02d688820e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6249,11 +6249,6 @@ console-control-strings@^1.1.0:
resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==
-"consolidated-events@^1.1.0 || ^2.0.0":
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/consolidated-events/-/consolidated-events-2.0.2.tgz#da8d8f8c2b232831413d9e190dc11669c79f4a91"
- integrity sha512-2/uRVMdRypf5z/TW/ncD/66l75P5hH2vM/GR8Jf8HLc2xnfJtmina6F6du8+v4Z2vTrMo7jC+W1tmEEuuELgkQ==
-
content-disposition@0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4"
@@ -14841,7 +14836,7 @@ promzard@^0.3.0:
dependencies:
read "1"
-prop-types@^15.0.0, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
+prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@@ -15052,7 +15047,7 @@ react-fast-compare@^3.2.0:
react-fast-compare "^3.2.0"
shallowequal "^1.1.0"
-"react-is@^16.12.0 || ^17.0.0 || ^18.0.0", "react-is@^17.0.1 || ^18.0.0", react-is@^18.0.0, react-is@^18.3.1:
+"react-is@^16.12.0 || ^17.0.0 || ^18.0.0", react-is@^18.0.0, react-is@^18.3.1:
version "18.3.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e"
integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==
@@ -15152,16 +15147,6 @@ react-test-renderer@^18.0.0:
react-shallow-renderer "^16.15.0"
scheduler "^0.23.2"
-react-waypoint@^10.3.0:
- version "10.3.0"
- resolved "https://registry.yarnpkg.com/react-waypoint/-/react-waypoint-10.3.0.tgz#fcc60e86c6c9ad2174fa58d066dc6ae54e3df71d"
- integrity sha512-iF1y2c1BsoXuEGz08NoahaLFIGI9gTUAAOKip96HUmylRT6DUtpgoBPjk/Y8dfcFVmfVDvUzWjNXpZyKTOV0SQ==
- dependencies:
- "@babel/runtime" "^7.12.5"
- consolidated-events "^1.1.0 || ^2.0.0"
- prop-types "^15.0.0"
- react-is "^17.0.1 || ^18.0.0"
-
react@16.14.0:
version "16.14.0"
resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d"