@pip-it-up/react 0.1.1 → 0.1.4

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,59 @@ 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
+ console.log("[PipWrapper] instance:", instance);
125
+ console.log("[PipWrapper] instance.setDefaultElements:", typeof instance.setDefaultElements);
126
+ if (typeof instance.setDefaultElements === "function") {
127
+ instance.setDefaultElements({
128
+ contentEl: contentRef.current || void 0,
129
+ originEl: originRef.current || void 0
130
+ });
131
+ } else {
132
+ console.error("[PipWrapper] setDefaultElements is MISSING on instance!");
133
+ }
134
+ }, [instance, state.isOpen]);
94
135
  const isControlled = controlledOpen !== void 0;
95
- const prevOpenRef = (0, import_react2.useRef)(state.isOpen);
96
- (0, import_react2.useEffect)(() => {
136
+ const prevOpenRef = (0, import_react3.useRef)(state.isOpen);
137
+ (0, import_react3.useEffect)(() => {
97
138
  if (state.isOpen !== prevOpenRef.current) {
98
139
  if (onOpenChange) {
99
140
  onOpenChange(state.isOpen);
@@ -101,48 +142,140 @@ var PipWrapper = (0, import_react2.forwardRef)((props, ref) => {
101
142
  prevOpenRef.current = state.isOpen;
102
143
  }
103
144
  }, [state.isOpen, onOpenChange]);
104
- (0, import_react2.useEffect)(() => {
145
+ const prevControlledOpenRef = (0, import_react3.useRef)(false);
146
+ (0, import_react3.useEffect)(() => {
105
147
  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) {
148
+ const changedToOpen = controlledOpen && !prevControlledOpenRef.current;
149
+ const changedToClosed = !controlledOpen && prevControlledOpenRef.current;
150
+ if (changedToOpen && !state.isOpen) {
151
+ if (contentRef.current) {
152
+ const rect = contentRef.current.getBoundingClientRect();
153
+ if (rect.width > 0 && rect.height > 0) {
154
+ lastSizeRef.current = { width: rect.width, height: rect.height };
155
+ }
156
+ }
157
+ instance.open().catch((err) => {
158
+ if (err.name === "NotAllowedError") {
159
+ console.warn('[PipWrapper] PiP window opening blocked: requires user activation. Ensure the "open" prop is changed within a user gesture handler.');
160
+ } else {
161
+ console.error("[PipWrapper] Failed to open PiP window", err);
162
+ }
163
+ });
164
+ } else if (changedToClosed && state.isOpen) {
109
165
  instance.close();
110
166
  }
167
+ prevControlledOpenRef.current = controlledOpen;
111
168
  }
112
169
  }, [controlledOpen, isControlled, state.isOpen, instance]);
113
- const defaultOpenHandled = (0, import_react2.useRef)(false);
114
- (0, import_react2.useEffect)(() => {
170
+ const defaultOpenHandled = (0, import_react3.useRef)(false);
171
+ (0, import_react3.useEffect)(() => {
115
172
  if (!isControlled && defaultOpen && !defaultOpenHandled.current) {
116
173
  defaultOpenHandled.current = true;
117
- instance.open({ contentEl: contentRef.current || void 0, originEl: originRef.current || void 0 });
174
+ instance.open().catch((err) => {
175
+ if (err.name === "NotAllowedError") {
176
+ console.warn("[PipWrapper] PiP window defaultOpen blocked: requires user activation.");
177
+ } else {
178
+ console.error("[PipWrapper] Failed to open PiP window via defaultOpen", err);
179
+ }
180
+ });
118
181
  }
119
182
  }, [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 }) }) });
183
+ const mode = coreOptions.mode === "clone" ? "move" : coreOptions.mode || "move";
184
+ 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: [
185
+ /* @__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" }),
186
+ /* @__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" })
187
+ ] });
188
+ const placeholderContent = placeholder !== void 0 ? placeholder : defaultPlaceholder;
189
+ const lastSizeRef = (0, import_react3.useRef)({ width: 0, height: 0 });
190
+ (0, import_react3.useEffect)(() => {
191
+ if (!contentRef.current || state.isOpen) return;
192
+ const observer = new ResizeObserver((entries) => {
193
+ for (const entry of entries) {
194
+ if (entry.contentRect.width > 0 && entry.contentRect.height > 0) {
195
+ lastSizeRef.current = {
196
+ width: entry.borderBoxSize?.[0]?.inlineSize ?? entry.contentRect.width,
197
+ height: entry.borderBoxSize?.[0]?.blockSize ?? entry.contentRect.height
198
+ };
199
+ }
200
+ }
201
+ });
202
+ observer.observe(contentRef.current);
203
+ return () => observer.disconnect();
204
+ }, [state.isOpen]);
205
+ const prevIsOpenRef = (0, import_react3.useRef)(state.isOpen);
206
+ (0, import_react3.useLayoutEffect)(() => {
207
+ if (prevIsOpenRef.current && !state.isOpen && contentRef.current) {
208
+ const rect = contentRef.current.getBoundingClientRect();
209
+ if (rect.width > 0 && rect.height > 0) {
210
+ lastSizeRef.current = { width: rect.width, height: rect.height };
211
+ }
212
+ }
213
+ prevIsOpenRef.current = state.isOpen;
214
+ }, [state.isOpen]);
215
+ (0, import_react3.useLayoutEffect)(() => {
216
+ if ((mode === "move" || mode === "portal") && state.isOpen && coreOptions.reserveSpace !== false && originRef.current) {
217
+ const { width, height } = lastSizeRef.current;
218
+ const origin = originRef.current;
219
+ if (width > 0 && height > 0) {
220
+ origin.style.minWidth = `${width}px`;
221
+ origin.style.minHeight = `${height}px`;
222
+ origin.style.width = `${width}px`;
223
+ origin.style.height = `${height}px`;
224
+ origin.style.display = "inline-block";
225
+ origin.style.verticalAlign = "top";
226
+ return () => {
227
+ origin.style.minWidth = "";
228
+ origin.style.minHeight = "";
229
+ origin.style.width = "";
230
+ origin.style.height = "";
231
+ origin.style.display = "";
232
+ origin.style.verticalAlign = "";
233
+ };
234
+ }
235
+ }
236
+ }, [mode, state.isOpen, coreOptions.reserveSpace, instance]);
237
+ 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: [
238
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
239
+ "div",
240
+ {
241
+ className: placeholderClassName,
242
+ style: {
243
+ width: coreOptions.width ? `${coreOptions.width}px` : "100%",
244
+ height: lastSizeRef.current.height ? `${lastSizeRef.current.height}px` : "auto",
245
+ display: "inline-block",
246
+ verticalAlign: "top",
247
+ boxSizing: "border-box"
248
+ },
249
+ children: placeholderContent
250
+ },
251
+ "placeholder"
252
+ ),
253
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(PipPortal, { pipWindow: state.pipWindow, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Component, { ref: contentRef, children }) })
254
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Component, { ref: contentRef, children }, "content") }) });
122
255
  });
123
256
  PipWrapper.displayName = "PipWrapper";
124
257
 
125
258
  // src/PipTrigger.tsx
126
- var import_react5 = require("react");
259
+ var import_react6 = __toESM(require("react"));
127
260
  var import_core3 = require("@pip-it-up/core");
128
261
 
129
262
  // src/useIsPipSupported.ts
130
- var import_react3 = require("react");
263
+ var import_react4 = require("react");
131
264
  var import_core2 = require("@pip-it-up/core");
132
265
  var useIsPipSupported = () => {
133
- const [supported, setSupported] = (0, import_react3.useState)(false);
134
- (0, import_react3.useEffect)(() => {
266
+ const [supported, setSupported] = (0, import_react4.useState)(false);
267
+ (0, import_react4.useEffect)(() => {
135
268
  setSupported((0, import_core2.isSupported)());
136
269
  }, []);
137
270
  return supported;
138
271
  };
139
272
 
140
273
  // src/Slot.tsx
141
- var import_react4 = __toESM(require("react"));
142
- var Slot = (0, import_react4.forwardRef)((props, ref) => {
274
+ var import_react5 = __toESM(require("react"));
275
+ var Slot = (0, import_react5.forwardRef)((props, ref) => {
143
276
  const { children, ...slotProps } = props;
144
- if (import_react4.default.isValidElement(children)) {
145
- return import_react4.default.cloneElement(children, {
277
+ if (import_react5.default.isValidElement(children)) {
278
+ return import_react5.default.cloneElement(children, {
146
279
  ...slotProps,
147
280
  ...children.props,
148
281
  style: {
@@ -153,26 +286,26 @@ var Slot = (0, import_react4.forwardRef)((props, ref) => {
153
286
  ref: (node) => {
154
287
  if (typeof ref === "function") ref(node);
155
288
  else if (ref) ref.current = node;
156
- const childRef = children.ref;
289
+ const childRef = children.props.ref || children.ref;
157
290
  if (typeof childRef === "function") childRef(node);
158
291
  else if (childRef) childRef.current = node;
159
292
  }
160
293
  });
161
294
  }
162
- if (import_react4.default.Children.count(children) > 1) {
163
- import_react4.default.Children.only(null);
295
+ if (import_react5.default.Children.count(children) > 1) {
296
+ throw new Error("PipTrigger asChild expects a single React element child");
164
297
  }
165
298
  return null;
166
299
  });
167
300
  Slot.displayName = "Slot";
168
301
 
169
302
  // src/PipTrigger.tsx
170
- var import_jsx_runtime2 = require("react/jsx-runtime");
303
+ var import_jsx_runtime3 = require("react/jsx-runtime");
171
304
  var emptySubscribe = () => () => {
172
305
  };
173
306
  var emptyServerState2 = { isOpen: false, isSupported: false, pipWindow: null };
174
307
  var emptyGetState = () => emptyServerState2;
175
- var PipTrigger = (0, import_react5.forwardRef)((props, ref) => {
308
+ var PipTrigger = (0, import_react6.forwardRef)((props, ref) => {
176
309
  const {
177
310
  pipId,
178
311
  asChild,
@@ -181,83 +314,116 @@ var PipTrigger = (0, import_react5.forwardRef)((props, ref) => {
181
314
  renderOpen,
182
315
  renderClose,
183
316
  renderUnsupported = null,
317
+ hideInPip = true,
184
318
  onClick,
185
319
  children,
186
320
  ...rest
187
321
  } = props;
188
322
  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
- }
323
+ const context = (0, import_react6.useContext)(PipContext);
324
+ const registrySubscribe = import_react6.default.useCallback((callback) => {
325
+ if (!pipId) return () => {
326
+ };
327
+ return (0, import_core3.subscribeRegistry)(pipId, callback);
199
328
  }, [pipId]);
329
+ const getRegistrySnapshot = import_react6.default.useCallback(() => {
330
+ return pipId ? (0, import_core3.getPip)(pipId) : null;
331
+ }, [pipId]);
332
+ const registryInstance = (0, import_react6.useSyncExternalStore)(
333
+ registrySubscribe,
334
+ getRegistrySnapshot,
335
+ () => null
336
+ );
200
337
  const instance = pipId ? registryInstance : context?.instance;
201
338
  const subscribe = instance?.subscribe || emptySubscribe;
202
339
  const getState = instance?.getState || emptyGetState;
203
- const state = (0, import_react5.useSyncExternalStore)(
340
+ const state = (0, import_react6.useSyncExternalStore)(
204
341
  subscribe,
205
342
  getState,
206
343
  emptyGetState
207
344
  );
208
- const [mounted, setMounted] = (0, import_react5.useState)(false);
209
- (0, import_react5.useEffect)(() => setMounted(true), []);
210
- if (!instance && pipId) {
211
- return null;
345
+ const [mounted, setMounted] = (0, import_react6.useState)(false);
346
+ (0, import_react6.useEffect)(() => setMounted(true), []);
347
+ if (!instance && pipId && mounted) {
348
+ const Comp2 = asChild ? Slot : "button";
349
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
350
+ Comp2,
351
+ {
352
+ ref,
353
+ disabled: true,
354
+ style: { opacity: 0.5, cursor: "not-allowed" },
355
+ ...asChild ? {} : { type: "button" },
356
+ ...rest,
357
+ children: asChild ? children : children ?? renderOpen ?? openLabel
358
+ }
359
+ );
212
360
  }
213
361
  if (!mounted) {
214
362
  return null;
215
363
  }
216
364
  if (!isSupported2) {
217
365
  if (renderUnsupported === null) return null;
218
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: renderUnsupported });
366
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_jsx_runtime3.Fragment, { children: renderUnsupported });
219
367
  }
220
- const handleClick = async (e) => {
368
+ const handleClick = (e) => {
221
369
  if (onClick) onClick(e);
222
- if (instance) {
223
- await instance.toggle();
224
- }
370
+ const target = instance ?? context?.instance;
371
+ target?.toggle();
225
372
  };
226
373
  const isOpen = state.isOpen;
374
+ if (hideInPip && isOpen && !!context) {
375
+ return null;
376
+ }
227
377
  const content = isOpen ? renderClose ?? closeLabel : renderOpen ?? openLabel;
228
378
  const Comp = asChild ? Slot : "button";
229
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
379
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
230
380
  Comp,
231
381
  {
232
382
  ref,
233
383
  onClick: handleClick,
234
384
  ...asChild ? {} : { type: "button" },
235
385
  ...rest,
236
- children: asChild ? children : content
386
+ children: asChild ? children : children ?? content
237
387
  }
238
388
  );
239
389
  });
240
390
  PipTrigger.displayName = "PipTrigger";
241
391
 
242
392
  // src/usePip.ts
243
- var import_react6 = require("react");
393
+ var import_react7 = require("react");
244
394
  var import_core4 = require("@pip-it-up/core");
245
395
  var emptyServerState3 = { isOpen: false, isSupported: false, pipWindow: null };
246
396
  var emptyGetState2 = () => emptyServerState3;
247
397
  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);
398
+ const contentRef = (0, import_react7.useRef)(null);
399
+ const originRef = (0, import_react7.useRef)(null);
400
+ const instanceRef = (0, import_react7.useRef)(null);
251
401
  if (!instanceRef.current) {
252
- instanceRef.current = (0, import_core4.createPip)(options);
402
+ const { id: _id, ...factoryOptions } = options;
403
+ instanceRef.current = (0, import_core4.createPip)(factoryOptions);
253
404
  }
254
405
  const instance = instanceRef.current;
255
- (0, import_react6.useEffect)(() => {
406
+ (0, import_react7.useEffect)(() => {
407
+ if (options.id) {
408
+ (0, import_core4.registerPip)(options.id, instance);
409
+ return () => (0, import_core4.unregisterPip)(options.id);
410
+ }
411
+ }, [options.id, instance]);
412
+ (0, import_react7.useEffect)(() => {
256
413
  return () => {
257
414
  instance.destroy();
415
+ instanceRef.current = null;
258
416
  };
259
417
  }, [instance]);
260
- const state = (0, import_react6.useSyncExternalStore)(
418
+ (0, import_react7.useLayoutEffect)(() => {
419
+ if (typeof instance.setDefaultElements === "function") {
420
+ instance.setDefaultElements({
421
+ contentEl: contentRef.current || void 0,
422
+ originEl: originRef.current || void 0
423
+ });
424
+ }
425
+ }, [instance]);
426
+ const state = (0, import_react7.useSyncExternalStore)(
261
427
  instance.subscribe,
262
428
  instance.getState,
263
429
  emptyGetState2
@@ -273,16 +439,6 @@ function usePip(options = {}) {
273
439
  pipWindow: state.pipWindow
274
440
  };
275
441
  }
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
442
  // Annotate the CommonJS export names for ESM import in node:
287
443
  0 && (module.exports = {
288
444
  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,47 @@ 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
+ console.log("[PipWrapper] instance:", instance);
84
+ console.log("[PipWrapper] instance.setDefaultElements:", typeof instance.setDefaultElements);
85
+ if (typeof instance.setDefaultElements === "function") {
86
+ instance.setDefaultElements({
87
+ contentEl: contentRef.current || void 0,
88
+ originEl: originRef.current || void 0
89
+ });
90
+ } else {
91
+ console.error("[PipWrapper] setDefaultElements is MISSING on instance!");
92
+ }
93
+ }, [instance, state.isOpen]);
53
94
  const isControlled = controlledOpen !== void 0;
54
95
  const prevOpenRef = useRef(state.isOpen);
55
96
  useEffect(() => {
@@ -60,29 +101,121 @@ var PipWrapper = forwardRef((props, ref) => {
60
101
  prevOpenRef.current = state.isOpen;
61
102
  }
62
103
  }, [state.isOpen, onOpenChange]);
104
+ const prevControlledOpenRef = useRef(false);
63
105
  useEffect(() => {
64
106
  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) {
107
+ const changedToOpen = controlledOpen && !prevControlledOpenRef.current;
108
+ const changedToClosed = !controlledOpen && prevControlledOpenRef.current;
109
+ if (changedToOpen && !state.isOpen) {
110
+ if (contentRef.current) {
111
+ const rect = contentRef.current.getBoundingClientRect();
112
+ if (rect.width > 0 && rect.height > 0) {
113
+ lastSizeRef.current = { width: rect.width, height: rect.height };
114
+ }
115
+ }
116
+ instance.open().catch((err) => {
117
+ if (err.name === "NotAllowedError") {
118
+ console.warn('[PipWrapper] PiP window opening blocked: requires user activation. Ensure the "open" prop is changed within a user gesture handler.');
119
+ } else {
120
+ console.error("[PipWrapper] Failed to open PiP window", err);
121
+ }
122
+ });
123
+ } else if (changedToClosed && state.isOpen) {
68
124
  instance.close();
69
125
  }
126
+ prevControlledOpenRef.current = controlledOpen;
70
127
  }
71
128
  }, [controlledOpen, isControlled, state.isOpen, instance]);
72
129
  const defaultOpenHandled = useRef(false);
73
130
  useEffect(() => {
74
131
  if (!isControlled && defaultOpen && !defaultOpenHandled.current) {
75
132
  defaultOpenHandled.current = true;
76
- instance.open({ contentEl: contentRef.current || void 0, originEl: originRef.current || void 0 });
133
+ instance.open().catch((err) => {
134
+ if (err.name === "NotAllowedError") {
135
+ console.warn("[PipWrapper] PiP window defaultOpen blocked: requires user activation.");
136
+ } else {
137
+ console.error("[PipWrapper] Failed to open PiP window via defaultOpen", err);
138
+ }
139
+ });
77
140
  }
78
141
  }, [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 }) }) });
142
+ const mode = coreOptions.mode === "clone" ? "move" : coreOptions.mode || "move";
143
+ 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: [
144
+ /* @__PURE__ */ jsx2("div", { style: { marginBottom: "4px", fontSize: "0.875rem", fontWeight: 500, opacity: 0.6, textAlign: "center" }, children: "\u{1F4FA} In PiP" }),
145
+ /* @__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" })
146
+ ] });
147
+ const placeholderContent = placeholder !== void 0 ? placeholder : defaultPlaceholder;
148
+ const lastSizeRef = useRef({ width: 0, height: 0 });
149
+ useEffect(() => {
150
+ if (!contentRef.current || state.isOpen) return;
151
+ const observer = new ResizeObserver((entries) => {
152
+ for (const entry of entries) {
153
+ if (entry.contentRect.width > 0 && entry.contentRect.height > 0) {
154
+ lastSizeRef.current = {
155
+ width: entry.borderBoxSize?.[0]?.inlineSize ?? entry.contentRect.width,
156
+ height: entry.borderBoxSize?.[0]?.blockSize ?? entry.contentRect.height
157
+ };
158
+ }
159
+ }
160
+ });
161
+ observer.observe(contentRef.current);
162
+ return () => observer.disconnect();
163
+ }, [state.isOpen]);
164
+ const prevIsOpenRef = useRef(state.isOpen);
165
+ useLayoutEffect(() => {
166
+ if (prevIsOpenRef.current && !state.isOpen && contentRef.current) {
167
+ const rect = contentRef.current.getBoundingClientRect();
168
+ if (rect.width > 0 && rect.height > 0) {
169
+ lastSizeRef.current = { width: rect.width, height: rect.height };
170
+ }
171
+ }
172
+ prevIsOpenRef.current = state.isOpen;
173
+ }, [state.isOpen]);
174
+ useLayoutEffect(() => {
175
+ if ((mode === "move" || mode === "portal") && state.isOpen && coreOptions.reserveSpace !== false && originRef.current) {
176
+ const { width, height } = lastSizeRef.current;
177
+ const origin = originRef.current;
178
+ if (width > 0 && height > 0) {
179
+ origin.style.minWidth = `${width}px`;
180
+ origin.style.minHeight = `${height}px`;
181
+ origin.style.width = `${width}px`;
182
+ origin.style.height = `${height}px`;
183
+ origin.style.display = "inline-block";
184
+ origin.style.verticalAlign = "top";
185
+ return () => {
186
+ origin.style.minWidth = "";
187
+ origin.style.minHeight = "";
188
+ origin.style.width = "";
189
+ origin.style.height = "";
190
+ origin.style.display = "";
191
+ origin.style.verticalAlign = "";
192
+ };
193
+ }
194
+ }
195
+ }, [mode, state.isOpen, coreOptions.reserveSpace, instance]);
196
+ 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: [
197
+ /* @__PURE__ */ jsx2(
198
+ "div",
199
+ {
200
+ className: placeholderClassName,
201
+ style: {
202
+ width: coreOptions.width ? `${coreOptions.width}px` : "100%",
203
+ height: lastSizeRef.current.height ? `${lastSizeRef.current.height}px` : "auto",
204
+ display: "inline-block",
205
+ verticalAlign: "top",
206
+ boxSizing: "border-box"
207
+ },
208
+ children: placeholderContent
209
+ },
210
+ "placeholder"
211
+ ),
212
+ /* @__PURE__ */ jsx2(PipPortal, { pipWindow: state.pipWindow, children: /* @__PURE__ */ jsx2(Component, { ref: contentRef, children }) })
213
+ ] }) : /* @__PURE__ */ jsx2(Component, { ref: contentRef, children }, "content") }) });
81
214
  });
82
215
  PipWrapper.displayName = "PipWrapper";
83
216
 
84
217
  // src/PipTrigger.tsx
85
- import { forwardRef as forwardRef3, useEffect as useEffect3, useState as useState2, useSyncExternalStore as useSyncExternalStore2, useContext } from "react";
218
+ import React3, { forwardRef as forwardRef3, useEffect as useEffect3, useState as useState2, useSyncExternalStore as useSyncExternalStore2, useContext as useContext2 } from "react";
86
219
  import { getPip, subscribeRegistry } from "@pip-it-up/core";
87
220
 
88
221
  // src/useIsPipSupported.ts
@@ -112,21 +245,21 @@ var Slot = forwardRef2((props, ref) => {
112
245
  ref: (node) => {
113
246
  if (typeof ref === "function") ref(node);
114
247
  else if (ref) ref.current = node;
115
- const childRef = children.ref;
248
+ const childRef = children.props.ref || children.ref;
116
249
  if (typeof childRef === "function") childRef(node);
117
250
  else if (childRef) childRef.current = node;
118
251
  }
119
252
  });
120
253
  }
121
254
  if (React2.Children.count(children) > 1) {
122
- React2.Children.only(null);
255
+ throw new Error("PipTrigger asChild expects a single React element child");
123
256
  }
124
257
  return null;
125
258
  });
126
259
  Slot.displayName = "Slot";
127
260
 
128
261
  // src/PipTrigger.tsx
129
- import { Fragment, jsx as jsx2 } from "react/jsx-runtime";
262
+ import { Fragment as Fragment2, jsx as jsx3 } from "react/jsx-runtime";
130
263
  var emptySubscribe = () => () => {
131
264
  };
132
265
  var emptyServerState2 = { isOpen: false, isSupported: false, pipWindow: null };
@@ -140,22 +273,26 @@ var PipTrigger = forwardRef3((props, ref) => {
140
273
  renderOpen,
141
274
  renderClose,
142
275
  renderUnsupported = null,
276
+ hideInPip = true,
143
277
  onClick,
144
278
  children,
145
279
  ...rest
146
280
  } = props;
147
281
  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
- }
282
+ const context = useContext2(PipContext);
283
+ const registrySubscribe = React3.useCallback((callback) => {
284
+ if (!pipId) return () => {
285
+ };
286
+ return subscribeRegistry(pipId, callback);
158
287
  }, [pipId]);
288
+ const getRegistrySnapshot = React3.useCallback(() => {
289
+ return pipId ? getPip(pipId) : null;
290
+ }, [pipId]);
291
+ const registryInstance = useSyncExternalStore2(
292
+ registrySubscribe,
293
+ getRegistrySnapshot,
294
+ () => null
295
+ );
159
296
  const instance = pipId ? registryInstance : context?.instance;
160
297
  const subscribe = instance?.subscribe || emptySubscribe;
161
298
  const getState = instance?.getState || emptyGetState;
@@ -166,41 +303,54 @@ var PipTrigger = forwardRef3((props, ref) => {
166
303
  );
167
304
  const [mounted, setMounted] = useState2(false);
168
305
  useEffect3(() => setMounted(true), []);
169
- if (!instance && pipId) {
170
- return null;
306
+ if (!instance && pipId && mounted) {
307
+ const Comp2 = asChild ? Slot : "button";
308
+ return /* @__PURE__ */ jsx3(
309
+ Comp2,
310
+ {
311
+ ref,
312
+ disabled: true,
313
+ style: { opacity: 0.5, cursor: "not-allowed" },
314
+ ...asChild ? {} : { type: "button" },
315
+ ...rest,
316
+ children: asChild ? children : children ?? renderOpen ?? openLabel
317
+ }
318
+ );
171
319
  }
172
320
  if (!mounted) {
173
321
  return null;
174
322
  }
175
323
  if (!isSupported2) {
176
324
  if (renderUnsupported === null) return null;
177
- return /* @__PURE__ */ jsx2(Fragment, { children: renderUnsupported });
325
+ return /* @__PURE__ */ jsx3(Fragment2, { children: renderUnsupported });
178
326
  }
179
- const handleClick = async (e) => {
327
+ const handleClick = (e) => {
180
328
  if (onClick) onClick(e);
181
- if (instance) {
182
- await instance.toggle();
183
- }
329
+ const target = instance ?? context?.instance;
330
+ target?.toggle();
184
331
  };
185
332
  const isOpen = state.isOpen;
333
+ if (hideInPip && isOpen && !!context) {
334
+ return null;
335
+ }
186
336
  const content = isOpen ? renderClose ?? closeLabel : renderOpen ?? openLabel;
187
337
  const Comp = asChild ? Slot : "button";
188
- return /* @__PURE__ */ jsx2(
338
+ return /* @__PURE__ */ jsx3(
189
339
  Comp,
190
340
  {
191
341
  ref,
192
342
  onClick: handleClick,
193
343
  ...asChild ? {} : { type: "button" },
194
344
  ...rest,
195
- children: asChild ? children : content
345
+ children: asChild ? children : children ?? content
196
346
  }
197
347
  );
198
348
  });
199
349
  PipTrigger.displayName = "PipTrigger";
200
350
 
201
351
  // 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";
352
+ import { useRef as useRef2, useEffect as useEffect4, useSyncExternalStore as useSyncExternalStore3, useLayoutEffect as useLayoutEffect2 } from "react";
353
+ import { createPip as createPip2, registerPip as registerPip2, unregisterPip as unregisterPip2 } from "@pip-it-up/core";
204
354
  var emptyServerState3 = { isOpen: false, isSupported: false, pipWindow: null };
205
355
  var emptyGetState2 = () => emptyServerState3;
206
356
  function usePip(options = {}) {
@@ -208,14 +358,30 @@ function usePip(options = {}) {
208
358
  const originRef = useRef2(null);
209
359
  const instanceRef = useRef2(null);
210
360
  if (!instanceRef.current) {
211
- instanceRef.current = createPip2(options);
361
+ const { id: _id, ...factoryOptions } = options;
362
+ instanceRef.current = createPip2(factoryOptions);
212
363
  }
213
364
  const instance = instanceRef.current;
365
+ useEffect4(() => {
366
+ if (options.id) {
367
+ registerPip2(options.id, instance);
368
+ return () => unregisterPip2(options.id);
369
+ }
370
+ }, [options.id, instance]);
214
371
  useEffect4(() => {
215
372
  return () => {
216
373
  instance.destroy();
374
+ instanceRef.current = null;
217
375
  };
218
376
  }, [instance]);
377
+ useLayoutEffect2(() => {
378
+ if (typeof instance.setDefaultElements === "function") {
379
+ instance.setDefaultElements({
380
+ contentEl: contentRef.current || void 0,
381
+ originEl: originRef.current || void 0
382
+ });
383
+ }
384
+ }, [instance]);
219
385
  const state = useSyncExternalStore3(
220
386
  instance.subscribe,
221
387
  instance.getState,
@@ -232,16 +398,6 @@ function usePip(options = {}) {
232
398
  pipWindow: state.pipWindow
233
399
  };
234
400
  }
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
401
  export {
246
402
  PipContext,
247
403
  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.4",
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.4"
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",