@pip-it-up/react 0.1.1 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,12 @@
1
1
  # @pip-it-up/react
2
2
 
3
- React bindings for the `pip-it-up` engine.
3
+ React bindings for `pip-it-up` — the **Document Picture-in-Picture** engine.
4
+
5
+ ## What is Document Picture-in-Picture?
6
+
7
+ The [Document Picture-in-Picture API](https://developer.mozilla.org/en-US/docs/Web/API/Document_Picture-in-Picture_API) is a new browser capability that allows you to open a floating window that can be populated with any arbitrary HTML content, rather than just a video element.
8
+
9
+ `@pip-it-up/react` makes it trivial to use this API in React applications with familiar patterns like Portals, Hooks, and Controlled Components.
4
10
 
5
11
  ## Installation
6
12
 
@@ -12,7 +18,7 @@ npm install @pip-it-up/react @pip-it-up/core
12
18
 
13
19
  ### `<PipWrapper>`
14
20
 
15
- Wraps the content you want to move into the PiP window.
21
+ Wraps the content you want to move into the **Picture-in-Picture** window.
16
22
 
17
23
  #### Uncontrolled (Default)
18
24
  ```tsx
@@ -32,34 +38,76 @@ const [isOpen, setIsOpen] = useState(false);
32
38
  ```
33
39
 
34
40
  #### Props
35
- Supports all `PipOptions` from `@pip-it-up/core` (`mode`, `copyStyles`, `fallback`, etc.), plus:
36
- - `open`: Controlled boolean state.
37
- - `onOpenChange`: Callback for controlled state.
38
- - `defaultOpen`: Initial uncontrolled state.
41
+
42
+ Supports all `PipOptions` from `@pip-it-up/core`, including:
43
+
44
+ - **`width` / `height`** (number, optional): If provided, forces the PiP window to these dimensions. **If omitted, the library uses a `ResizeObserver` to automatically match the component's exact size on the page.**
45
+ - **`mode`** (`"move" | "portal"`, default: `"move"`):
46
+ - In this React package, both `"move"` and `"portal"` utilize **React Portals** to move content safely between windows. They behave identically: a placeholder is left in the original spot, and the component stays in the same React tree.
47
+ - **`copyStyles`** (`"sync" | "once" | false`, default: `"sync"`):
48
+ - `"sync"`: Real-time synchronization of CSS changes (MutationObserver).
49
+ - `"once"`: One-time copy at window open.
50
+ - **`reserveSpace`** (boolean, default: `true`): Whether to show a placeholder in the original position to prevent layout jumps.
51
+ - **`placeholder`** (ReactNode): Custom component to show in the placeholder area.
52
+ - **`centerInPip`** (boolean, default: `false`): Automatically centers your content in the PiP window.
53
+ - **`open`** (boolean): Controlled state for the window.
54
+ - **`onOpenChange`** (callback): Fired when the window opens or closes.
39
55
 
40
56
  ### `<PipTrigger>`
41
57
 
42
- A button that toggles the PiP window.
58
+ A button that toggles the **Picture-in-Picture** window.
43
59
 
44
60
  ```tsx
45
61
  <PipTrigger asChild>
46
- <button className="my-custom-btn">Open PiP</button>
62
+ <button className="my-custom-btn">Open Picture-in-Picture</button>
47
63
  </PipTrigger>
48
64
  ```
49
65
 
50
66
  ## Hooks
51
67
 
52
68
  ### `usePip()`
53
- Returns the context state.
69
+ Returns the context state for managing the **Picture-in-Picture** lifecycle.
54
70
  ```tsx
55
- const { isOpen, pipWindow, instance } = usePip();
71
+ const { isOpen, pipWindow, instance, isInsidePip } = usePip();
56
72
  ```
73
+ - **`isOpen`**: Boolean indicating if the PiP window is open.
74
+ - **`pipWindow`**: The native `Window` object of the PiP instance (null if closed).
75
+ - **`isInsidePip`**: Boolean that is `true` only when the component is being rendered inside the PiP window.
76
+ - **`instance`**: The underlying `@pip-it-up/core` instance.
77
+
78
+ > [!NOTE]
79
+ > `usePipContext()` is also available if you only need the raw context without the extra convenience properties of `usePip()`.
57
80
 
58
81
  ### `useIsPipSupported()`
59
- Returns boolean if the browser natively supports Document PiP.
82
+ Returns `true` if the browser natively supports the **Document Picture-in-Picture API**.
60
83
  ```tsx
61
84
  const isSupported = useIsPipSupported();
62
85
  ```
63
86
 
87
+ ## Tips & Gotchas
88
+
89
+ ### CSS Inheritance
90
+ The Picture-in-Picture window is a separate document. While `@pip-it-up` automatically copies stylesheets and `body`/`html` classes, your content will **not** inherit styles from parent elements outside the `<PipWrapper>` (like a `#root` div or a theme provider).
91
+ * **Fix**: Add necessary alignment or theme classes (e.g., `text-center`, `dark`) directly to the content inside the `<PipWrapper>`.
92
+
93
+ ### Complex Editors (Monaco, TipTap, Canvas)
94
+ Editors that bind to the `document` object at initialization may break when moved to a new window.
95
+ * **Fix 1**: Force a remount of the editor when moving to PiP by using the `isInsidePip` state as a React `key`.
96
+ * **Fix 2**: Lift your editor state (content, cursors, etc.) **above** the `<PipWrapper>`. Since the wrapper unmounts and remounts its children when moving them into the PiP window, any state stored *inside* the wrapper will be lost during the transition.
97
+
98
+ ```tsx
99
+ const [content, setContent] = useState("");
100
+
101
+ return (
102
+ <PipWrapper>
103
+ <RichTextEditor
104
+ key={isInsidePip ? 'pip' : 'main'}
105
+ value={content}
106
+ onChange={setContent}
107
+ />
108
+ </PipWrapper>
109
+ );
110
+ ```
111
+
64
112
  ## Next.js / SSR
65
- Because the Document PiP API is browser-only, ensure components interacting with it are rendered on the client (`"use client"`).
113
+ Because the **Document Picture-in-Picture API** is browser-only, ensure components interacting with it are rendered on the client (`"use client"`).
package/dist/index.d.mts CHANGED
@@ -2,14 +2,44 @@ import * as React from 'react';
2
2
  import React__default, { ElementType, ReactNode } from 'react';
3
3
  import { PipOptions, PipInstance, PipState } from '@pip-it-up/core';
4
4
 
5
- interface PipWrapperProps extends PipOptions {
5
+ interface PipWrapperProps extends Omit<PipOptions, 'mode'> {
6
6
  open?: boolean;
7
7
  defaultOpen?: boolean;
8
8
  onOpenChange?: (open: boolean) => void;
9
9
  as?: ElementType;
10
10
  originAs?: ElementType;
11
11
  children?: ReactNode;
12
+ placeholder?: ReactNode;
13
+ placeholderClassName?: string;
14
+ /**
15
+ * @deprecated `mode` is not supported in PipWrapper. The React package always
16
+ * uses portal mode internally because React manages its own DOM — vanilla
17
+ * `move`/`clone` modes would conflict with React's reconciler. For clone mode,
18
+ * use the vanilla `createPip()` API directly.
19
+ */
20
+ mode?: PipOptions['mode'];
12
21
  }
22
+ /**
23
+ * A React wrapper for the Document Picture-in-Picture API.
24
+ *
25
+ * **Important**: The underlying core instance always uses `mode: 'portal'`
26
+ * regardless of any `mode` prop. This is because React handles DOM movement
27
+ * via its own Portal system — using the core's vanilla DOM `move`/`clone`
28
+ * modes would cause `removeChild` errors and reconciliation crashes.
29
+ *
30
+ * **Implication for vanilla interop**: If you register a `<PipWrapper id="foo">`
31
+ * and then call `getPip('foo').open({ contentEl, originEl })` from vanilla JS,
32
+ * the core runs in portal mode — meaning `applyMoveMode` and `applyCloneMode`
33
+ * are skipped. The vanilla caller would get an empty PiP window. For vanilla
34
+ * DOM manipulation, create a separate instance with `createPip()` directly.
35
+ *
36
+ * **Layout note**: The outer wrapper element (`originAs`, defaults to `div`)
37
+ * uses `display: contents` to avoid adding an extra layout box. This means:
38
+ * - `getBoundingClientRect()` on the forwarded ref will return all zeros.
39
+ * - Screen readers may skip the element.
40
+ * If you need to measure the wrapper, measure the inner content element
41
+ * (the `as` element) instead.
42
+ */
13
43
  declare const PipWrapper: React__default.ForwardRefExoticComponent<PipWrapperProps & React__default.RefAttributes<HTMLElement>>;
14
44
 
15
45
  interface PipTriggerProps extends React__default.ButtonHTMLAttributes<HTMLButtonElement> {
@@ -20,6 +50,7 @@ interface PipTriggerProps extends React__default.ButtonHTMLAttributes<HTMLButton
20
50
  renderOpen?: React__default.ReactNode;
21
51
  renderClose?: React__default.ReactNode;
22
52
  renderUnsupported?: React__default.ReactNode | null;
53
+ hideInPip?: boolean;
23
54
  }
24
55
  declare const PipTrigger: React__default.ForwardRefExoticComponent<PipTriggerProps & React__default.RefAttributes<HTMLElement>>;
25
56
 
@@ -34,13 +65,14 @@ declare function usePip<T extends HTMLElement = HTMLDivElement>(options?: PipOpt
34
65
  pipWindow: Window | null;
35
66
  };
36
67
 
37
- interface PipContextValue<T = unknown> {
68
+ interface PipContextValue {
38
69
  instance: PipInstance;
39
70
  state: PipState;
71
+ isInsidePip: boolean;
40
72
  }
41
- declare const PipContext: React.Context<PipContextValue<any> | null>;
73
+ declare const PipContext: React.Context<PipContextValue | null>;
42
74
 
43
- declare function usePipContext<T = unknown>(): PipContextValue<T>;
75
+ declare function usePipContext(): PipContextValue;
44
76
 
45
77
  declare const useIsPipSupported: () => boolean;
46
78
 
package/dist/index.d.ts CHANGED
@@ -2,14 +2,44 @@ import * as React from 'react';
2
2
  import React__default, { ElementType, ReactNode } from 'react';
3
3
  import { PipOptions, PipInstance, PipState } from '@pip-it-up/core';
4
4
 
5
- interface PipWrapperProps extends PipOptions {
5
+ interface PipWrapperProps extends Omit<PipOptions, 'mode'> {
6
6
  open?: boolean;
7
7
  defaultOpen?: boolean;
8
8
  onOpenChange?: (open: boolean) => void;
9
9
  as?: ElementType;
10
10
  originAs?: ElementType;
11
11
  children?: ReactNode;
12
+ placeholder?: ReactNode;
13
+ placeholderClassName?: string;
14
+ /**
15
+ * @deprecated `mode` is not supported in PipWrapper. The React package always
16
+ * uses portal mode internally because React manages its own DOM — vanilla
17
+ * `move`/`clone` modes would conflict with React's reconciler. For clone mode,
18
+ * use the vanilla `createPip()` API directly.
19
+ */
20
+ mode?: PipOptions['mode'];
12
21
  }
22
+ /**
23
+ * A React wrapper for the Document Picture-in-Picture API.
24
+ *
25
+ * **Important**: The underlying core instance always uses `mode: 'portal'`
26
+ * regardless of any `mode` prop. This is because React handles DOM movement
27
+ * via its own Portal system — using the core's vanilla DOM `move`/`clone`
28
+ * modes would cause `removeChild` errors and reconciliation crashes.
29
+ *
30
+ * **Implication for vanilla interop**: If you register a `<PipWrapper id="foo">`
31
+ * and then call `getPip('foo').open({ contentEl, originEl })` from vanilla JS,
32
+ * the core runs in portal mode — meaning `applyMoveMode` and `applyCloneMode`
33
+ * are skipped. The vanilla caller would get an empty PiP window. For vanilla
34
+ * DOM manipulation, create a separate instance with `createPip()` directly.
35
+ *
36
+ * **Layout note**: The outer wrapper element (`originAs`, defaults to `div`)
37
+ * uses `display: contents` to avoid adding an extra layout box. This means:
38
+ * - `getBoundingClientRect()` on the forwarded ref will return all zeros.
39
+ * - Screen readers may skip the element.
40
+ * If you need to measure the wrapper, measure the inner content element
41
+ * (the `as` element) instead.
42
+ */
13
43
  declare const PipWrapper: React__default.ForwardRefExoticComponent<PipWrapperProps & React__default.RefAttributes<HTMLElement>>;
14
44
 
15
45
  interface PipTriggerProps extends React__default.ButtonHTMLAttributes<HTMLButtonElement> {
@@ -20,6 +50,7 @@ interface PipTriggerProps extends React__default.ButtonHTMLAttributes<HTMLButton
20
50
  renderOpen?: React__default.ReactNode;
21
51
  renderClose?: React__default.ReactNode;
22
52
  renderUnsupported?: React__default.ReactNode | null;
53
+ hideInPip?: boolean;
23
54
  }
24
55
  declare const PipTrigger: React__default.ForwardRefExoticComponent<PipTriggerProps & React__default.RefAttributes<HTMLElement>>;
25
56
 
@@ -34,13 +65,14 @@ declare function usePip<T extends HTMLElement = HTMLDivElement>(options?: PipOpt
34
65
  pipWindow: Window | null;
35
66
  };
36
67
 
37
- interface PipContextValue<T = unknown> {
68
+ interface PipContextValue {
38
69
  instance: PipInstance;
39
70
  state: PipState;
71
+ isInsidePip: boolean;
40
72
  }
41
- declare const PipContext: React.Context<PipContextValue<any> | null>;
73
+ declare const PipContext: React.Context<PipContextValue | null>;
42
74
 
43
- declare function usePipContext<T = unknown>(): PipContextValue<T>;
75
+ declare function usePipContext(): PipContextValue;
44
76
 
45
77
  declare const useIsPipSupported: () => boolean;
46
78
 
package/dist/index.js CHANGED
@@ -40,7 +40,7 @@ __export(index_exports, {
40
40
  module.exports = __toCommonJS(index_exports);
41
41
 
42
42
  // src/PipWrapper.tsx
43
- var import_react2 = require("react");
43
+ var import_react3 = require("react");
44
44
  var import_core = require("@pip-it-up/core");
45
45
 
46
46
  // src/PipContext.tsx
@@ -49,15 +49,32 @@ var PipContext = (0, import_react.createContext)(null);
49
49
 
50
50
  // src/PipPortal.tsx
51
51
  var import_react_dom = require("react-dom");
52
+
53
+ // src/usePipContext.ts
54
+ var import_react2 = require("react");
55
+ function usePipContext() {
56
+ const context = (0, import_react2.useContext)(PipContext);
57
+ if (!context) {
58
+ throw new Error("usePipContext must be used within a <PipWrapper>");
59
+ }
60
+ return context;
61
+ }
62
+
63
+ // src/PipPortal.tsx
64
+ var import_jsx_runtime = require("react/jsx-runtime");
52
65
  var PipPortal = ({ children, pipWindow }) => {
53
- return (0, import_react_dom.createPortal)(children, pipWindow.document.body);
66
+ const context = usePipContext();
67
+ return (0, import_react_dom.createPortal)(
68
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PipContext.Provider, { value: { ...context, isInsidePip: true }, children }),
69
+ pipWindow.document.body
70
+ );
54
71
  };
55
72
 
56
73
  // src/PipWrapper.tsx
57
- var import_jsx_runtime = require("react/jsx-runtime");
74
+ var import_jsx_runtime2 = require("react/jsx-runtime");
58
75
  var emptyServerState = { isOpen: false, isSupported: false, pipWindow: null };
59
76
  var getServerState = () => emptyServerState;
60
- var PipWrapper = (0, import_react2.forwardRef)((props, ref) => {
77
+ var PipWrapper = (0, import_react3.forwardRef)((props, ref) => {
61
78
  const {
62
79
  open: controlledOpen,
63
80
  defaultOpen = false,
@@ -65,35 +82,57 @@ var PipWrapper = (0, import_react2.forwardRef)((props, ref) => {
65
82
  as: Component = "div",
66
83
  originAs: OriginComponent = "div",
67
84
  children,
85
+ placeholder,
86
+ placeholderClassName,
68
87
  ...coreOptions
69
88
  } = props;
70
- const contentRef = (0, import_react2.useRef)(null);
71
- const originRef = (0, import_react2.useRef)(null);
72
- const instanceRef = (0, import_react2.useRef)(null);
73
- (0, import_react2.useImperativeHandle)(ref, () => originRef.current);
89
+ const contentRef = (0, import_react3.useRef)(null);
90
+ const originRef = (0, import_react3.useRef)(null);
91
+ const instanceRef = (0, import_react3.useRef)(null);
92
+ (0, import_react3.useImperativeHandle)(ref, () => originRef.current);
74
93
  if (!instanceRef.current) {
75
- instanceRef.current = (0, import_core.createPip)(coreOptions);
94
+ const { id: _id, mode: _mode, ...factoryOptions } = coreOptions;
95
+ if (_mode === "clone") {
96
+ console.warn(
97
+ '[PipWrapper] mode="clone" is not supported in the React package. PipWrapper always uses portal mode internally. For clone mode, use the vanilla createPip() API directly. Falling back to portal mode.'
98
+ );
99
+ }
100
+ instanceRef.current = (0, import_core.createPip)({ ...factoryOptions, mode: "portal" });
76
101
  }
77
102
  const instance = instanceRef.current;
78
- (0, import_react2.useEffect)(() => {
79
- instance.updateElements({
80
- contentEl: contentRef.current || void 0,
81
- originEl: originRef.current || void 0
82
- });
83
- });
84
- (0, import_react2.useEffect)(() => {
103
+ (0, import_react3.useEffect)(() => {
104
+ if (coreOptions.id) {
105
+ (0, import_core.registerPip)(coreOptions.id, instance);
106
+ return () => {
107
+ (0, import_core.unregisterPip)(coreOptions.id);
108
+ };
109
+ }
110
+ }, [coreOptions.id, instance]);
111
+ (0, import_react3.useEffect)(() => {
85
112
  return () => {
86
- instance.destroy();
113
+ if (instanceRef.current) {
114
+ instanceRef.current.destroy();
115
+ }
87
116
  };
88
- }, [instance]);
89
- const state = (0, import_react2.useSyncExternalStore)(
117
+ }, []);
118
+ const state = (0, import_react3.useSyncExternalStore)(
90
119
  instance.subscribe,
91
120
  instance.getState,
92
121
  getServerState
93
122
  );
123
+ (0, import_react3.useLayoutEffect)(() => {
124
+ if (typeof instance.setDefaultElements === "function") {
125
+ instance.setDefaultElements({
126
+ contentEl: contentRef.current || void 0,
127
+ originEl: originRef.current || void 0
128
+ });
129
+ } else {
130
+ console.error("[PipWrapper] setDefaultElements is MISSING on instance!");
131
+ }
132
+ }, [instance, state.isOpen]);
94
133
  const isControlled = controlledOpen !== void 0;
95
- const prevOpenRef = (0, import_react2.useRef)(state.isOpen);
96
- (0, import_react2.useEffect)(() => {
134
+ const prevOpenRef = (0, import_react3.useRef)(state.isOpen);
135
+ (0, import_react3.useEffect)(() => {
97
136
  if (state.isOpen !== prevOpenRef.current) {
98
137
  if (onOpenChange) {
99
138
  onOpenChange(state.isOpen);
@@ -101,48 +140,140 @@ var PipWrapper = (0, import_react2.forwardRef)((props, ref) => {
101
140
  prevOpenRef.current = state.isOpen;
102
141
  }
103
142
  }, [state.isOpen, onOpenChange]);
104
- (0, import_react2.useEffect)(() => {
143
+ const prevControlledOpenRef = (0, import_react3.useRef)(false);
144
+ (0, import_react3.useEffect)(() => {
105
145
  if (isControlled) {
106
- if (controlledOpen && !state.isOpen) {
107
- instance.open({ contentEl: contentRef.current || void 0, originEl: originRef.current || void 0 });
108
- } else if (!controlledOpen && state.isOpen) {
146
+ const changedToOpen = controlledOpen && !prevControlledOpenRef.current;
147
+ const changedToClosed = !controlledOpen && prevControlledOpenRef.current;
148
+ if (changedToOpen && !state.isOpen) {
149
+ if (contentRef.current) {
150
+ const rect = contentRef.current.getBoundingClientRect();
151
+ if (rect.width > 0 && rect.height > 0) {
152
+ lastSizeRef.current = { width: rect.width, height: rect.height };
153
+ }
154
+ }
155
+ instance.open().catch((err) => {
156
+ if (err.name === "NotAllowedError") {
157
+ console.warn('[PipWrapper] PiP window opening blocked: requires user activation. Ensure the "open" prop is changed within a user gesture handler.');
158
+ } else {
159
+ console.error("[PipWrapper] Failed to open PiP window", err);
160
+ }
161
+ });
162
+ } else if (changedToClosed && state.isOpen) {
109
163
  instance.close();
110
164
  }
165
+ prevControlledOpenRef.current = controlledOpen;
111
166
  }
112
167
  }, [controlledOpen, isControlled, state.isOpen, instance]);
113
- const defaultOpenHandled = (0, import_react2.useRef)(false);
114
- (0, import_react2.useEffect)(() => {
168
+ const defaultOpenHandled = (0, import_react3.useRef)(false);
169
+ (0, import_react3.useEffect)(() => {
115
170
  if (!isControlled && defaultOpen && !defaultOpenHandled.current) {
116
171
  defaultOpenHandled.current = true;
117
- instance.open({ contentEl: contentRef.current || void 0, originEl: originRef.current || void 0 });
172
+ instance.open().catch((err) => {
173
+ if (err.name === "NotAllowedError") {
174
+ console.warn("[PipWrapper] PiP window defaultOpen blocked: requires user activation.");
175
+ } else {
176
+ console.error("[PipWrapper] Failed to open PiP window via defaultOpen", err);
177
+ }
178
+ });
118
179
  }
119
180
  }, [defaultOpen, isControlled, instance]);
120
- const mode = coreOptions.mode || "move";
121
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PipContext.Provider, { value: { instance, state }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(OriginComponent, { ref: originRef, children: mode === "portal" && state.isOpen && state.pipWindow ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PipPortal, { pipWindow: state.pipWindow, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Component, { ref: contentRef, children }) }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Component, { ref: contentRef, children }) }) });
181
+ const mode = coreOptions.mode === "clone" ? "move" : coreOptions.mode || "move";
182
+ const defaultPlaceholder = /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { padding: "12px", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", backgroundColor: "transparent", border: "1px dashed color-mix(in srgb, currentColor 30%, #ccc)", borderRadius: "inherit", width: "100%", height: "100%", boxSizing: "border-box", overflow: "hidden" }, children: [
183
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { marginBottom: "4px", fontSize: "0.875rem", fontWeight: 500, opacity: 0.6, textAlign: "center" }, children: "\u{1F4FA} In PiP" }),
184
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { onClick: () => instance.close(), style: { fontSize: "0.75rem", padding: "4px 8px", cursor: "pointer", borderRadius: "4px", border: "1px solid currentColor", background: "transparent", opacity: 0.6 }, children: "Restore" })
185
+ ] });
186
+ const placeholderContent = placeholder !== void 0 ? placeholder : defaultPlaceholder;
187
+ const lastSizeRef = (0, import_react3.useRef)({ width: 0, height: 0 });
188
+ (0, import_react3.useEffect)(() => {
189
+ if (!contentRef.current || state.isOpen) return;
190
+ const observer = new ResizeObserver((entries) => {
191
+ for (const entry of entries) {
192
+ if (entry.contentRect.width > 0 && entry.contentRect.height > 0) {
193
+ lastSizeRef.current = {
194
+ width: entry.borderBoxSize?.[0]?.inlineSize ?? entry.contentRect.width,
195
+ height: entry.borderBoxSize?.[0]?.blockSize ?? entry.contentRect.height
196
+ };
197
+ }
198
+ }
199
+ });
200
+ observer.observe(contentRef.current);
201
+ return () => observer.disconnect();
202
+ }, [state.isOpen]);
203
+ const prevIsOpenRef = (0, import_react3.useRef)(state.isOpen);
204
+ (0, import_react3.useLayoutEffect)(() => {
205
+ if (prevIsOpenRef.current && !state.isOpen && contentRef.current) {
206
+ const rect = contentRef.current.getBoundingClientRect();
207
+ if (rect.width > 0 && rect.height > 0) {
208
+ lastSizeRef.current = { width: rect.width, height: rect.height };
209
+ }
210
+ }
211
+ prevIsOpenRef.current = state.isOpen;
212
+ }, [state.isOpen]);
213
+ (0, import_react3.useLayoutEffect)(() => {
214
+ if ((mode === "move" || mode === "portal") && state.isOpen && coreOptions.reserveSpace !== false && originRef.current) {
215
+ const { width, height } = lastSizeRef.current;
216
+ const origin = originRef.current;
217
+ if (width > 0 && height > 0) {
218
+ origin.style.minWidth = `${width}px`;
219
+ origin.style.minHeight = `${height}px`;
220
+ origin.style.width = `${width}px`;
221
+ origin.style.height = `${height}px`;
222
+ origin.style.display = "inline-block";
223
+ origin.style.verticalAlign = "top";
224
+ return () => {
225
+ origin.style.minWidth = "";
226
+ origin.style.minHeight = "";
227
+ origin.style.width = "";
228
+ origin.style.height = "";
229
+ origin.style.display = "";
230
+ origin.style.verticalAlign = "";
231
+ };
232
+ }
233
+ }
234
+ }, [mode, state.isOpen, coreOptions.reserveSpace, instance]);
235
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(PipContext.Provider, { value: { instance, state, isInsidePip: false }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(OriginComponent, { ref: originRef, style: { display: "contents" }, children: (mode === "portal" || mode === "move") && state.isOpen && state.pipWindow ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
236
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
237
+ "div",
238
+ {
239
+ className: placeholderClassName,
240
+ style: {
241
+ width: coreOptions.width ? `${coreOptions.width}px` : "100%",
242
+ height: lastSizeRef.current.height ? `${lastSizeRef.current.height}px` : "auto",
243
+ display: "inline-block",
244
+ verticalAlign: "top",
245
+ boxSizing: "border-box"
246
+ },
247
+ children: placeholderContent
248
+ },
249
+ "placeholder"
250
+ ),
251
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(PipPortal, { pipWindow: state.pipWindow, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Component, { ref: contentRef, children }) })
252
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Component, { ref: contentRef, children }, "content") }) });
122
253
  });
123
254
  PipWrapper.displayName = "PipWrapper";
124
255
 
125
256
  // src/PipTrigger.tsx
126
- var import_react5 = require("react");
257
+ var import_react6 = __toESM(require("react"));
127
258
  var import_core3 = require("@pip-it-up/core");
128
259
 
129
260
  // src/useIsPipSupported.ts
130
- var import_react3 = require("react");
261
+ var import_react4 = require("react");
131
262
  var import_core2 = require("@pip-it-up/core");
132
263
  var useIsPipSupported = () => {
133
- const [supported, setSupported] = (0, import_react3.useState)(false);
134
- (0, import_react3.useEffect)(() => {
264
+ const [supported, setSupported] = (0, import_react4.useState)(false);
265
+ (0, import_react4.useEffect)(() => {
135
266
  setSupported((0, import_core2.isSupported)());
136
267
  }, []);
137
268
  return supported;
138
269
  };
139
270
 
140
271
  // src/Slot.tsx
141
- var import_react4 = __toESM(require("react"));
142
- var Slot = (0, import_react4.forwardRef)((props, ref) => {
272
+ var import_react5 = __toESM(require("react"));
273
+ var Slot = (0, import_react5.forwardRef)((props, ref) => {
143
274
  const { children, ...slotProps } = props;
144
- if (import_react4.default.isValidElement(children)) {
145
- return import_react4.default.cloneElement(children, {
275
+ if (import_react5.default.isValidElement(children)) {
276
+ return import_react5.default.cloneElement(children, {
146
277
  ...slotProps,
147
278
  ...children.props,
148
279
  style: {
@@ -153,26 +284,26 @@ var Slot = (0, import_react4.forwardRef)((props, ref) => {
153
284
  ref: (node) => {
154
285
  if (typeof ref === "function") ref(node);
155
286
  else if (ref) ref.current = node;
156
- const childRef = children.ref;
287
+ const childRef = children.props.ref || children.ref;
157
288
  if (typeof childRef === "function") childRef(node);
158
289
  else if (childRef) childRef.current = node;
159
290
  }
160
291
  });
161
292
  }
162
- if (import_react4.default.Children.count(children) > 1) {
163
- import_react4.default.Children.only(null);
293
+ if (import_react5.default.Children.count(children) > 1) {
294
+ throw new Error("PipTrigger asChild expects a single React element child");
164
295
  }
165
296
  return null;
166
297
  });
167
298
  Slot.displayName = "Slot";
168
299
 
169
300
  // src/PipTrigger.tsx
170
- var import_jsx_runtime2 = require("react/jsx-runtime");
301
+ var import_jsx_runtime3 = require("react/jsx-runtime");
171
302
  var emptySubscribe = () => () => {
172
303
  };
173
304
  var emptyServerState2 = { isOpen: false, isSupported: false, pipWindow: null };
174
305
  var emptyGetState = () => emptyServerState2;
175
- var PipTrigger = (0, import_react5.forwardRef)((props, ref) => {
306
+ var PipTrigger = (0, import_react6.forwardRef)((props, ref) => {
176
307
  const {
177
308
  pipId,
178
309
  asChild,
@@ -181,83 +312,116 @@ var PipTrigger = (0, import_react5.forwardRef)((props, ref) => {
181
312
  renderOpen,
182
313
  renderClose,
183
314
  renderUnsupported = null,
315
+ hideInPip = true,
184
316
  onClick,
185
317
  children,
186
318
  ...rest
187
319
  } = props;
188
320
  const isSupported2 = useIsPipSupported();
189
- const context = (0, import_react5.useContext)(PipContext);
190
- const [registryInstance, setRegistryInstance] = (0, import_react5.useState)(pipId ? (0, import_core3.getPip)(pipId) : null);
191
- (0, import_react5.useEffect)(() => {
192
- if (pipId) {
193
- setRegistryInstance((0, import_core3.getPip)(pipId));
194
- const unsub = (0, import_core3.subscribeRegistry)(pipId, () => {
195
- setRegistryInstance((0, import_core3.getPip)(pipId));
196
- });
197
- return unsub;
198
- }
321
+ const context = (0, import_react6.useContext)(PipContext);
322
+ const registrySubscribe = import_react6.default.useCallback((callback) => {
323
+ if (!pipId) return () => {
324
+ };
325
+ return (0, import_core3.subscribeRegistry)(pipId, callback);
199
326
  }, [pipId]);
327
+ const getRegistrySnapshot = import_react6.default.useCallback(() => {
328
+ return pipId ? (0, import_core3.getPip)(pipId) : null;
329
+ }, [pipId]);
330
+ const registryInstance = (0, import_react6.useSyncExternalStore)(
331
+ registrySubscribe,
332
+ getRegistrySnapshot,
333
+ () => null
334
+ );
200
335
  const instance = pipId ? registryInstance : context?.instance;
201
336
  const subscribe = instance?.subscribe || emptySubscribe;
202
337
  const getState = instance?.getState || emptyGetState;
203
- const state = (0, import_react5.useSyncExternalStore)(
338
+ const state = (0, import_react6.useSyncExternalStore)(
204
339
  subscribe,
205
340
  getState,
206
341
  emptyGetState
207
342
  );
208
- const [mounted, setMounted] = (0, import_react5.useState)(false);
209
- (0, import_react5.useEffect)(() => setMounted(true), []);
210
- if (!instance && pipId) {
211
- return null;
343
+ const [mounted, setMounted] = (0, import_react6.useState)(false);
344
+ (0, import_react6.useEffect)(() => setMounted(true), []);
345
+ if (!instance && pipId && mounted) {
346
+ const Comp2 = asChild ? Slot : "button";
347
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
348
+ Comp2,
349
+ {
350
+ ref,
351
+ disabled: true,
352
+ style: { opacity: 0.5, cursor: "not-allowed" },
353
+ ...asChild ? {} : { type: "button" },
354
+ ...rest,
355
+ children: asChild ? children : children ?? renderOpen ?? openLabel
356
+ }
357
+ );
212
358
  }
213
359
  if (!mounted) {
214
360
  return null;
215
361
  }
216
362
  if (!isSupported2) {
217
363
  if (renderUnsupported === null) return null;
218
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: renderUnsupported });
364
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_jsx_runtime3.Fragment, { children: renderUnsupported });
219
365
  }
220
- const handleClick = async (e) => {
366
+ const handleClick = (e) => {
221
367
  if (onClick) onClick(e);
222
- if (instance) {
223
- await instance.toggle();
224
- }
368
+ const target = instance ?? context?.instance;
369
+ target?.toggle();
225
370
  };
226
371
  const isOpen = state.isOpen;
372
+ if (hideInPip && isOpen && !!context) {
373
+ return null;
374
+ }
227
375
  const content = isOpen ? renderClose ?? closeLabel : renderOpen ?? openLabel;
228
376
  const Comp = asChild ? Slot : "button";
229
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
377
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
230
378
  Comp,
231
379
  {
232
380
  ref,
233
381
  onClick: handleClick,
234
382
  ...asChild ? {} : { type: "button" },
235
383
  ...rest,
236
- children: asChild ? children : content
384
+ children: asChild ? children : children ?? content
237
385
  }
238
386
  );
239
387
  });
240
388
  PipTrigger.displayName = "PipTrigger";
241
389
 
242
390
  // src/usePip.ts
243
- var import_react6 = require("react");
391
+ var import_react7 = require("react");
244
392
  var import_core4 = require("@pip-it-up/core");
245
393
  var emptyServerState3 = { isOpen: false, isSupported: false, pipWindow: null };
246
394
  var emptyGetState2 = () => emptyServerState3;
247
395
  function usePip(options = {}) {
248
- const contentRef = (0, import_react6.useRef)(null);
249
- const originRef = (0, import_react6.useRef)(null);
250
- const instanceRef = (0, import_react6.useRef)(null);
396
+ const contentRef = (0, import_react7.useRef)(null);
397
+ const originRef = (0, import_react7.useRef)(null);
398
+ const instanceRef = (0, import_react7.useRef)(null);
251
399
  if (!instanceRef.current) {
252
- instanceRef.current = (0, import_core4.createPip)(options);
400
+ const { id: _id, ...factoryOptions } = options;
401
+ instanceRef.current = (0, import_core4.createPip)(factoryOptions);
253
402
  }
254
403
  const instance = instanceRef.current;
255
- (0, import_react6.useEffect)(() => {
404
+ (0, import_react7.useEffect)(() => {
405
+ if (options.id) {
406
+ (0, import_core4.registerPip)(options.id, instance);
407
+ return () => (0, import_core4.unregisterPip)(options.id);
408
+ }
409
+ }, [options.id, instance]);
410
+ (0, import_react7.useEffect)(() => {
256
411
  return () => {
257
412
  instance.destroy();
413
+ instanceRef.current = null;
258
414
  };
259
415
  }, [instance]);
260
- const state = (0, import_react6.useSyncExternalStore)(
416
+ (0, import_react7.useLayoutEffect)(() => {
417
+ if (typeof instance.setDefaultElements === "function") {
418
+ instance.setDefaultElements({
419
+ contentEl: contentRef.current || void 0,
420
+ originEl: originRef.current || void 0
421
+ });
422
+ }
423
+ }, [instance]);
424
+ const state = (0, import_react7.useSyncExternalStore)(
261
425
  instance.subscribe,
262
426
  instance.getState,
263
427
  emptyGetState2
@@ -273,16 +437,6 @@ function usePip(options = {}) {
273
437
  pipWindow: state.pipWindow
274
438
  };
275
439
  }
276
-
277
- // src/usePipContext.ts
278
- var import_react7 = require("react");
279
- function usePipContext() {
280
- const context = (0, import_react7.useContext)(PipContext);
281
- if (!context) {
282
- throw new Error("usePipContext must be used within a <PipWrapper>");
283
- }
284
- return context;
285
- }
286
440
  // Annotate the CommonJS export names for ESM import in node:
287
441
  0 && (module.exports = {
288
442
  PipContext,
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/PipWrapper.tsx
2
- import { forwardRef, useEffect, useRef, useSyncExternalStore, useImperativeHandle } from "react";
3
- import { createPip } from "@pip-it-up/core";
2
+ import { forwardRef, useEffect, useRef, useSyncExternalStore, useImperativeHandle, useLayoutEffect } from "react";
3
+ import { createPip, registerPip, unregisterPip } from "@pip-it-up/core";
4
4
 
5
5
  // src/PipContext.tsx
6
6
  import { createContext } from "react";
@@ -8,12 +8,29 @@ var PipContext = createContext(null);
8
8
 
9
9
  // src/PipPortal.tsx
10
10
  import { createPortal } from "react-dom";
11
+
12
+ // src/usePipContext.ts
13
+ import { useContext } from "react";
14
+ function usePipContext() {
15
+ const context = useContext(PipContext);
16
+ if (!context) {
17
+ throw new Error("usePipContext must be used within a <PipWrapper>");
18
+ }
19
+ return context;
20
+ }
21
+
22
+ // src/PipPortal.tsx
23
+ import { jsx } from "react/jsx-runtime";
11
24
  var PipPortal = ({ children, pipWindow }) => {
12
- return createPortal(children, pipWindow.document.body);
25
+ const context = usePipContext();
26
+ return createPortal(
27
+ /* @__PURE__ */ jsx(PipContext.Provider, { value: { ...context, isInsidePip: true }, children }),
28
+ pipWindow.document.body
29
+ );
13
30
  };
14
31
 
15
32
  // src/PipWrapper.tsx
16
- import { jsx } from "react/jsx-runtime";
33
+ import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
17
34
  var emptyServerState = { isOpen: false, isSupported: false, pipWindow: null };
18
35
  var getServerState = () => emptyServerState;
19
36
  var PipWrapper = forwardRef((props, ref) => {
@@ -24,6 +41,8 @@ var PipWrapper = forwardRef((props, ref) => {
24
41
  as: Component = "div",
25
42
  originAs: OriginComponent = "div",
26
43
  children,
44
+ placeholder,
45
+ placeholderClassName,
27
46
  ...coreOptions
28
47
  } = props;
29
48
  const contentRef = useRef(null);
@@ -31,25 +50,45 @@ var PipWrapper = forwardRef((props, ref) => {
31
50
  const instanceRef = useRef(null);
32
51
  useImperativeHandle(ref, () => originRef.current);
33
52
  if (!instanceRef.current) {
34
- instanceRef.current = createPip(coreOptions);
53
+ const { id: _id, mode: _mode, ...factoryOptions } = coreOptions;
54
+ if (_mode === "clone") {
55
+ console.warn(
56
+ '[PipWrapper] mode="clone" is not supported in the React package. PipWrapper always uses portal mode internally. For clone mode, use the vanilla createPip() API directly. Falling back to portal mode.'
57
+ );
58
+ }
59
+ instanceRef.current = createPip({ ...factoryOptions, mode: "portal" });
35
60
  }
36
61
  const instance = instanceRef.current;
37
62
  useEffect(() => {
38
- instance.updateElements({
39
- contentEl: contentRef.current || void 0,
40
- originEl: originRef.current || void 0
41
- });
42
- });
63
+ if (coreOptions.id) {
64
+ registerPip(coreOptions.id, instance);
65
+ return () => {
66
+ unregisterPip(coreOptions.id);
67
+ };
68
+ }
69
+ }, [coreOptions.id, instance]);
43
70
  useEffect(() => {
44
71
  return () => {
45
- instance.destroy();
72
+ if (instanceRef.current) {
73
+ instanceRef.current.destroy();
74
+ }
46
75
  };
47
- }, [instance]);
76
+ }, []);
48
77
  const state = useSyncExternalStore(
49
78
  instance.subscribe,
50
79
  instance.getState,
51
80
  getServerState
52
81
  );
82
+ useLayoutEffect(() => {
83
+ if (typeof instance.setDefaultElements === "function") {
84
+ instance.setDefaultElements({
85
+ contentEl: contentRef.current || void 0,
86
+ originEl: originRef.current || void 0
87
+ });
88
+ } else {
89
+ console.error("[PipWrapper] setDefaultElements is MISSING on instance!");
90
+ }
91
+ }, [instance, state.isOpen]);
53
92
  const isControlled = controlledOpen !== void 0;
54
93
  const prevOpenRef = useRef(state.isOpen);
55
94
  useEffect(() => {
@@ -60,29 +99,121 @@ var PipWrapper = forwardRef((props, ref) => {
60
99
  prevOpenRef.current = state.isOpen;
61
100
  }
62
101
  }, [state.isOpen, onOpenChange]);
102
+ const prevControlledOpenRef = useRef(false);
63
103
  useEffect(() => {
64
104
  if (isControlled) {
65
- if (controlledOpen && !state.isOpen) {
66
- instance.open({ contentEl: contentRef.current || void 0, originEl: originRef.current || void 0 });
67
- } else if (!controlledOpen && state.isOpen) {
105
+ const changedToOpen = controlledOpen && !prevControlledOpenRef.current;
106
+ const changedToClosed = !controlledOpen && prevControlledOpenRef.current;
107
+ if (changedToOpen && !state.isOpen) {
108
+ if (contentRef.current) {
109
+ const rect = contentRef.current.getBoundingClientRect();
110
+ if (rect.width > 0 && rect.height > 0) {
111
+ lastSizeRef.current = { width: rect.width, height: rect.height };
112
+ }
113
+ }
114
+ instance.open().catch((err) => {
115
+ if (err.name === "NotAllowedError") {
116
+ console.warn('[PipWrapper] PiP window opening blocked: requires user activation. Ensure the "open" prop is changed within a user gesture handler.');
117
+ } else {
118
+ console.error("[PipWrapper] Failed to open PiP window", err);
119
+ }
120
+ });
121
+ } else if (changedToClosed && state.isOpen) {
68
122
  instance.close();
69
123
  }
124
+ prevControlledOpenRef.current = controlledOpen;
70
125
  }
71
126
  }, [controlledOpen, isControlled, state.isOpen, instance]);
72
127
  const defaultOpenHandled = useRef(false);
73
128
  useEffect(() => {
74
129
  if (!isControlled && defaultOpen && !defaultOpenHandled.current) {
75
130
  defaultOpenHandled.current = true;
76
- instance.open({ contentEl: contentRef.current || void 0, originEl: originRef.current || void 0 });
131
+ instance.open().catch((err) => {
132
+ if (err.name === "NotAllowedError") {
133
+ console.warn("[PipWrapper] PiP window defaultOpen blocked: requires user activation.");
134
+ } else {
135
+ console.error("[PipWrapper] Failed to open PiP window via defaultOpen", err);
136
+ }
137
+ });
77
138
  }
78
139
  }, [defaultOpen, isControlled, instance]);
79
- const mode = coreOptions.mode || "move";
80
- return /* @__PURE__ */ jsx(PipContext.Provider, { value: { instance, state }, children: /* @__PURE__ */ jsx(OriginComponent, { ref: originRef, children: mode === "portal" && state.isOpen && state.pipWindow ? /* @__PURE__ */ jsx(PipPortal, { pipWindow: state.pipWindow, children: /* @__PURE__ */ jsx(Component, { ref: contentRef, children }) }) : /* @__PURE__ */ jsx(Component, { ref: contentRef, children }) }) });
140
+ const mode = coreOptions.mode === "clone" ? "move" : coreOptions.mode || "move";
141
+ const defaultPlaceholder = /* @__PURE__ */ jsxs("div", { style: { padding: "12px", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", backgroundColor: "transparent", border: "1px dashed color-mix(in srgb, currentColor 30%, #ccc)", borderRadius: "inherit", width: "100%", height: "100%", boxSizing: "border-box", overflow: "hidden" }, children: [
142
+ /* @__PURE__ */ jsx2("div", { style: { marginBottom: "4px", fontSize: "0.875rem", fontWeight: 500, opacity: 0.6, textAlign: "center" }, children: "\u{1F4FA} In PiP" }),
143
+ /* @__PURE__ */ jsx2("button", { onClick: () => instance.close(), style: { fontSize: "0.75rem", padding: "4px 8px", cursor: "pointer", borderRadius: "4px", border: "1px solid currentColor", background: "transparent", opacity: 0.6 }, children: "Restore" })
144
+ ] });
145
+ const placeholderContent = placeholder !== void 0 ? placeholder : defaultPlaceholder;
146
+ const lastSizeRef = useRef({ width: 0, height: 0 });
147
+ useEffect(() => {
148
+ if (!contentRef.current || state.isOpen) return;
149
+ const observer = new ResizeObserver((entries) => {
150
+ for (const entry of entries) {
151
+ if (entry.contentRect.width > 0 && entry.contentRect.height > 0) {
152
+ lastSizeRef.current = {
153
+ width: entry.borderBoxSize?.[0]?.inlineSize ?? entry.contentRect.width,
154
+ height: entry.borderBoxSize?.[0]?.blockSize ?? entry.contentRect.height
155
+ };
156
+ }
157
+ }
158
+ });
159
+ observer.observe(contentRef.current);
160
+ return () => observer.disconnect();
161
+ }, [state.isOpen]);
162
+ const prevIsOpenRef = useRef(state.isOpen);
163
+ useLayoutEffect(() => {
164
+ if (prevIsOpenRef.current && !state.isOpen && contentRef.current) {
165
+ const rect = contentRef.current.getBoundingClientRect();
166
+ if (rect.width > 0 && rect.height > 0) {
167
+ lastSizeRef.current = { width: rect.width, height: rect.height };
168
+ }
169
+ }
170
+ prevIsOpenRef.current = state.isOpen;
171
+ }, [state.isOpen]);
172
+ useLayoutEffect(() => {
173
+ if ((mode === "move" || mode === "portal") && state.isOpen && coreOptions.reserveSpace !== false && originRef.current) {
174
+ const { width, height } = lastSizeRef.current;
175
+ const origin = originRef.current;
176
+ if (width > 0 && height > 0) {
177
+ origin.style.minWidth = `${width}px`;
178
+ origin.style.minHeight = `${height}px`;
179
+ origin.style.width = `${width}px`;
180
+ origin.style.height = `${height}px`;
181
+ origin.style.display = "inline-block";
182
+ origin.style.verticalAlign = "top";
183
+ return () => {
184
+ origin.style.minWidth = "";
185
+ origin.style.minHeight = "";
186
+ origin.style.width = "";
187
+ origin.style.height = "";
188
+ origin.style.display = "";
189
+ origin.style.verticalAlign = "";
190
+ };
191
+ }
192
+ }
193
+ }, [mode, state.isOpen, coreOptions.reserveSpace, instance]);
194
+ return /* @__PURE__ */ jsx2(PipContext.Provider, { value: { instance, state, isInsidePip: false }, children: /* @__PURE__ */ jsx2(OriginComponent, { ref: originRef, style: { display: "contents" }, children: (mode === "portal" || mode === "move") && state.isOpen && state.pipWindow ? /* @__PURE__ */ jsxs(Fragment, { children: [
195
+ /* @__PURE__ */ jsx2(
196
+ "div",
197
+ {
198
+ className: placeholderClassName,
199
+ style: {
200
+ width: coreOptions.width ? `${coreOptions.width}px` : "100%",
201
+ height: lastSizeRef.current.height ? `${lastSizeRef.current.height}px` : "auto",
202
+ display: "inline-block",
203
+ verticalAlign: "top",
204
+ boxSizing: "border-box"
205
+ },
206
+ children: placeholderContent
207
+ },
208
+ "placeholder"
209
+ ),
210
+ /* @__PURE__ */ jsx2(PipPortal, { pipWindow: state.pipWindow, children: /* @__PURE__ */ jsx2(Component, { ref: contentRef, children }) })
211
+ ] }) : /* @__PURE__ */ jsx2(Component, { ref: contentRef, children }, "content") }) });
81
212
  });
82
213
  PipWrapper.displayName = "PipWrapper";
83
214
 
84
215
  // src/PipTrigger.tsx
85
- import { forwardRef as forwardRef3, useEffect as useEffect3, useState as useState2, useSyncExternalStore as useSyncExternalStore2, useContext } from "react";
216
+ import React3, { forwardRef as forwardRef3, useEffect as useEffect3, useState as useState2, useSyncExternalStore as useSyncExternalStore2, useContext as useContext2 } from "react";
86
217
  import { getPip, subscribeRegistry } from "@pip-it-up/core";
87
218
 
88
219
  // src/useIsPipSupported.ts
@@ -112,21 +243,21 @@ var Slot = forwardRef2((props, ref) => {
112
243
  ref: (node) => {
113
244
  if (typeof ref === "function") ref(node);
114
245
  else if (ref) ref.current = node;
115
- const childRef = children.ref;
246
+ const childRef = children.props.ref || children.ref;
116
247
  if (typeof childRef === "function") childRef(node);
117
248
  else if (childRef) childRef.current = node;
118
249
  }
119
250
  });
120
251
  }
121
252
  if (React2.Children.count(children) > 1) {
122
- React2.Children.only(null);
253
+ throw new Error("PipTrigger asChild expects a single React element child");
123
254
  }
124
255
  return null;
125
256
  });
126
257
  Slot.displayName = "Slot";
127
258
 
128
259
  // src/PipTrigger.tsx
129
- import { Fragment, jsx as jsx2 } from "react/jsx-runtime";
260
+ import { Fragment as Fragment2, jsx as jsx3 } from "react/jsx-runtime";
130
261
  var emptySubscribe = () => () => {
131
262
  };
132
263
  var emptyServerState2 = { isOpen: false, isSupported: false, pipWindow: null };
@@ -140,22 +271,26 @@ var PipTrigger = forwardRef3((props, ref) => {
140
271
  renderOpen,
141
272
  renderClose,
142
273
  renderUnsupported = null,
274
+ hideInPip = true,
143
275
  onClick,
144
276
  children,
145
277
  ...rest
146
278
  } = props;
147
279
  const isSupported2 = useIsPipSupported();
148
- const context = useContext(PipContext);
149
- const [registryInstance, setRegistryInstance] = useState2(pipId ? getPip(pipId) : null);
150
- useEffect3(() => {
151
- if (pipId) {
152
- setRegistryInstance(getPip(pipId));
153
- const unsub = subscribeRegistry(pipId, () => {
154
- setRegistryInstance(getPip(pipId));
155
- });
156
- return unsub;
157
- }
280
+ const context = useContext2(PipContext);
281
+ const registrySubscribe = React3.useCallback((callback) => {
282
+ if (!pipId) return () => {
283
+ };
284
+ return subscribeRegistry(pipId, callback);
158
285
  }, [pipId]);
286
+ const getRegistrySnapshot = React3.useCallback(() => {
287
+ return pipId ? getPip(pipId) : null;
288
+ }, [pipId]);
289
+ const registryInstance = useSyncExternalStore2(
290
+ registrySubscribe,
291
+ getRegistrySnapshot,
292
+ () => null
293
+ );
159
294
  const instance = pipId ? registryInstance : context?.instance;
160
295
  const subscribe = instance?.subscribe || emptySubscribe;
161
296
  const getState = instance?.getState || emptyGetState;
@@ -166,41 +301,54 @@ var PipTrigger = forwardRef3((props, ref) => {
166
301
  );
167
302
  const [mounted, setMounted] = useState2(false);
168
303
  useEffect3(() => setMounted(true), []);
169
- if (!instance && pipId) {
170
- return null;
304
+ if (!instance && pipId && mounted) {
305
+ const Comp2 = asChild ? Slot : "button";
306
+ return /* @__PURE__ */ jsx3(
307
+ Comp2,
308
+ {
309
+ ref,
310
+ disabled: true,
311
+ style: { opacity: 0.5, cursor: "not-allowed" },
312
+ ...asChild ? {} : { type: "button" },
313
+ ...rest,
314
+ children: asChild ? children : children ?? renderOpen ?? openLabel
315
+ }
316
+ );
171
317
  }
172
318
  if (!mounted) {
173
319
  return null;
174
320
  }
175
321
  if (!isSupported2) {
176
322
  if (renderUnsupported === null) return null;
177
- return /* @__PURE__ */ jsx2(Fragment, { children: renderUnsupported });
323
+ return /* @__PURE__ */ jsx3(Fragment2, { children: renderUnsupported });
178
324
  }
179
- const handleClick = async (e) => {
325
+ const handleClick = (e) => {
180
326
  if (onClick) onClick(e);
181
- if (instance) {
182
- await instance.toggle();
183
- }
327
+ const target = instance ?? context?.instance;
328
+ target?.toggle();
184
329
  };
185
330
  const isOpen = state.isOpen;
331
+ if (hideInPip && isOpen && !!context) {
332
+ return null;
333
+ }
186
334
  const content = isOpen ? renderClose ?? closeLabel : renderOpen ?? openLabel;
187
335
  const Comp = asChild ? Slot : "button";
188
- return /* @__PURE__ */ jsx2(
336
+ return /* @__PURE__ */ jsx3(
189
337
  Comp,
190
338
  {
191
339
  ref,
192
340
  onClick: handleClick,
193
341
  ...asChild ? {} : { type: "button" },
194
342
  ...rest,
195
- children: asChild ? children : content
343
+ children: asChild ? children : children ?? content
196
344
  }
197
345
  );
198
346
  });
199
347
  PipTrigger.displayName = "PipTrigger";
200
348
 
201
349
  // src/usePip.ts
202
- import { useRef as useRef2, useEffect as useEffect4, useSyncExternalStore as useSyncExternalStore3 } from "react";
203
- import { createPip as createPip2 } from "@pip-it-up/core";
350
+ import { useRef as useRef2, useEffect as useEffect4, useSyncExternalStore as useSyncExternalStore3, useLayoutEffect as useLayoutEffect2 } from "react";
351
+ import { createPip as createPip2, registerPip as registerPip2, unregisterPip as unregisterPip2 } from "@pip-it-up/core";
204
352
  var emptyServerState3 = { isOpen: false, isSupported: false, pipWindow: null };
205
353
  var emptyGetState2 = () => emptyServerState3;
206
354
  function usePip(options = {}) {
@@ -208,14 +356,30 @@ function usePip(options = {}) {
208
356
  const originRef = useRef2(null);
209
357
  const instanceRef = useRef2(null);
210
358
  if (!instanceRef.current) {
211
- instanceRef.current = createPip2(options);
359
+ const { id: _id, ...factoryOptions } = options;
360
+ instanceRef.current = createPip2(factoryOptions);
212
361
  }
213
362
  const instance = instanceRef.current;
363
+ useEffect4(() => {
364
+ if (options.id) {
365
+ registerPip2(options.id, instance);
366
+ return () => unregisterPip2(options.id);
367
+ }
368
+ }, [options.id, instance]);
214
369
  useEffect4(() => {
215
370
  return () => {
216
371
  instance.destroy();
372
+ instanceRef.current = null;
217
373
  };
218
374
  }, [instance]);
375
+ useLayoutEffect2(() => {
376
+ if (typeof instance.setDefaultElements === "function") {
377
+ instance.setDefaultElements({
378
+ contentEl: contentRef.current || void 0,
379
+ originEl: originRef.current || void 0
380
+ });
381
+ }
382
+ }, [instance]);
219
383
  const state = useSyncExternalStore3(
220
384
  instance.subscribe,
221
385
  instance.getState,
@@ -232,16 +396,6 @@ function usePip(options = {}) {
232
396
  pipWindow: state.pipWindow
233
397
  };
234
398
  }
235
-
236
- // src/usePipContext.ts
237
- import { useContext as useContext2 } from "react";
238
- function usePipContext() {
239
- const context = useContext2(PipContext);
240
- if (!context) {
241
- throw new Error("usePipContext must be used within a <PipWrapper>");
242
- }
243
- return context;
244
- }
245
399
  export {
246
400
  PipContext,
247
401
  PipTrigger,
package/package.json CHANGED
@@ -1,6 +1,19 @@
1
1
  {
2
2
  "name": "@pip-it-up/react",
3
- "version": "0.1.1",
3
+ "version": "0.1.5",
4
+ "description": "React components and hooks for the Document Picture-in-Picture API — PipWrapper, PipTrigger, usePip, useIsPipSupported",
5
+ "keywords": [
6
+ "picture-in-picture",
7
+ "pip",
8
+ "react-pip",
9
+ "document-picture-in-picture",
10
+ "react-picture-in-picture",
11
+ "floating-window",
12
+ "pip-window",
13
+ "react-hooks",
14
+ "react-components",
15
+ "document-pip"
16
+ ],
4
17
  "repository": {
5
18
  "type": "git",
6
19
  "url": "git+https://github.com/Shakya47/pip-it-up.git",
@@ -29,11 +42,11 @@
29
42
  "dist"
30
43
  ],
31
44
  "dependencies": {
32
- "@pip-it-up/core": "0.1.1"
45
+ "@pip-it-up/core": "0.1.5"
33
46
  },
34
47
  "peerDependencies": {
35
- "react": ">=17",
36
- "react-dom": ">=17"
48
+ "react": ">=18",
49
+ "react-dom": ">=18"
37
50
  },
38
51
  "devDependencies": {
39
52
  "@testing-library/jest-dom": "^6.9.1",