@ai-matrx/capture 0.4.2 → 0.5.0

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/dist/react.d.cts CHANGED
@@ -40,6 +40,14 @@ interface CaptureCloudPort {
40
40
  /** Opens the host's cloud media library (tiled gallery). */
41
41
  onOpenLibrary: () => void;
42
42
  }
43
+ /** One media item in the cloud library sheet. The host supplies the
44
+ * thumbnail NODE (its own resolver/auth) — the package never fetches. */
45
+ interface CaptureCloudLibraryItem {
46
+ id: string;
47
+ fileName: string;
48
+ kind: "image" | "video";
49
+ thumbnail: React.ReactNode;
50
+ }
43
51
  /**
44
52
  * The engine port — everything the chrome needs from the host's camera
45
53
  * runtime. The host owns lease acquisition/release, capture and recording;
@@ -709,17 +717,270 @@ interface TrackControls {
709
717
  declare function useTrackControls(stream: MediaStream | null): TrackControls;
710
718
 
711
719
  interface DefaultEngineOptions {
712
- /** Receives every captured photo as a JPEG File. */
720
+ /** Receives every captured photo as a JPEG File (full sensor, aspect crop
721
+ * already applied). */
713
722
  onPhoto: (file: File) => void;
714
723
  /** Receives the finished recording. */
715
724
  onVideo: (file: File, durationMs: number) => void;
716
725
  /** Receives files chosen through the Upload lane. */
717
726
  onFiles: (files: FileList) => void;
718
- /** Record microphone audio with video (asks once, at record time). */
727
+ /** Record microphone audio with video. Default true. With audio on, the
728
+ * engine folds the mic permission into the CAMERA's browser prompt (one
729
+ * combined prompt) whenever a mic prompt would actually appear. */
719
730
  withAudio?: boolean;
731
+ /** The chrome's current mode. In "video" the engine warm-holds the mic so
732
+ * iOS Safari prompts at most once per medium per session. Omitting it
733
+ * skips the warm hold (the record path still acquires the mic). */
734
+ mode?: CaptureCameraMode;
720
735
  facingMode?: "environment" | "user";
721
736
  photoQuality?: number;
737
+ /** Filename prefix for captures (default "capture"). */
738
+ fileNamePrefix?: string;
739
+ /** Loud failure sink (a toast, a logger). Default: console.error. A tapped
740
+ * shutter that produced nothing must say so — never a silent drop. */
741
+ onError?: (message: string, err: unknown) => void;
742
+ }
743
+ /** The engine plus the extras the default path owns (white shutter flash). */
744
+ interface DefaultCaptureEngine extends CaptureCameraEngine {
745
+ /** White shutter-flash flag (120 ms), for the host's flash overlay. */
746
+ flash: boolean;
747
+ /** Take a photo with a non-default filename prefix (e.g. delineators). */
748
+ capturePhotoWith: (opts: {
749
+ fileNamePrefix: string;
750
+ aspect?: CaptureAspect;
751
+ }) => void;
752
+ }
753
+ declare function useDefaultCaptureEngine(options: DefaultEngineOptions): DefaultCaptureEngine;
754
+
755
+ /**
756
+ * src/engine/crop.ts — the ONE aspect-crop implementation.
757
+ *
758
+ * Center-crops a captured JPEG to the requested output aspect. The crop is
759
+ * applied to the FULL sensor frame (honest pixels, not a preview grab) —
760
+ * §2 policy 6 of the chrome: the shutter always captures full-sensor and the
761
+ * selected aspect is a REAL crop of that frame. Ported from the matrx-frontend
762
+ * host adapter (C22 retrofit, 2026-08-30) so every host shares one geometry.
763
+ */
764
+
765
+ declare const PHOTO_JPEG_QUALITY = 0.92;
766
+ declare function cropBlobToAspect(blob: Blob, aspect: CaptureAspect, quality?: number): Promise<Blob>;
767
+ /**
768
+ * Cycle to the next camera in a device list — the flip algorithm the host
769
+ * adapter and the default engine share. Returns null when there is nothing
770
+ * to flip to (fewer than two cameras).
771
+ */
772
+ declare function nextCameraDevice<T extends {
773
+ deviceId: string;
774
+ }>(cameras: readonly T[], currentDeviceId: string | null | undefined): T | null;
775
+
776
+ /**
777
+ * src/engine/permissions.ts — permission pre-checks + error classification
778
+ * for the default engine (and for hosts, so they stop re-implementing the
779
+ * NotAllowedError branch — the C22 rule: quirk branches live HERE).
780
+ */
781
+ type MediaPermissionState = "granted" | "denied" | "prompt" | "unknown";
782
+ /**
783
+ * Pre-check the camera permission WITHOUT prompting. A known-"denied" state
784
+ * lets a surface render its how-to-re-enable explainer instead of hammering
785
+ * getUserMedia with calls that instantly reject (the proven matrx-frontend
786
+ * pre-check contract). "unknown" (Safari) means: just try — the gUM outcome
787
+ * is the truth there.
788
+ */
789
+ declare function queryCameraPermission(): Promise<MediaPermissionState>;
790
+ /** Same pre-check for the microphone — drives the combined-prompt decision. */
791
+ declare function queryMicPermission(): Promise<MediaPermissionState>;
792
+ /**
793
+ * Pure decision for the combined camera+mic prompt: only worth it when a mic
794
+ * prompt would actually appear ("prompt"/"unknown"). "granted" needs no
795
+ * prompt; "denied" would reject the WHOLE combined call and take the camera
796
+ * with it.
797
+ */
798
+ declare function shouldCombineMicPrompt(micPermissionState: MediaPermissionState): boolean;
799
+ /** True when a getUserMedia rejection means the USER/OS denied permission
800
+ * (a missing device is not a denial). */
801
+ declare function isMediaDenialError(err: unknown): boolean;
802
+ /**
803
+ * Classify a camera-acquisition failure into the engine's `blocked` reason.
804
+ * The ONE place the NotAllowedError/SecurityError branch lives — hosts adapt
805
+ * their runtime errors through this instead of re-implementing it.
806
+ */
807
+ declare function classifyCameraBlockReason(err: unknown): "permission-denied" | "not-supported";
808
+
809
+ /**
810
+ * src/engine/warm-mic.ts — the package's warm microphone manager.
811
+ *
812
+ * Ported VERBATIM (2026-08-30, the C22/C23 retrofit) from matrx-frontend's
813
+ * proven `features/audio/micStream.ts` — the module that encodes the real
814
+ * iOS Safari device pain the default engine previously did not have:
815
+ *
816
+ * WHY THIS EXISTS
817
+ * ---------------
818
+ * Every recording surface used to call `navigator.mediaDevices.getUserMedia`
819
+ * itself and fully `track.stop()` the stream when it finished. On mobile
820
+ * (notably iOS Safari) that produces a permission/"in use" prompt on *every*
821
+ * single recording, because the previous grant is torn down before the next
822
+ * acquisition. Tapping record three times = three prompts.
823
+ *
824
+ * This manager keeps ONE stream warm and hands it to every caller. After the
825
+ * last holder releases it, the stream is kept alive for a SHORT keepalive
826
+ * window; if another recording starts within that window, the same live
827
+ * stream (and the same OS grant) is reused — no second prompt. Only after the
828
+ * window elapses with no holders is the stream actually stopped (mic light
829
+ * off), so we don't hold the mic open forever.
830
+ *
831
+ * KEEPALIVE IS DELIBERATELY SHORT (a few seconds). Minutes keep the browser's
832
+ * recording indicator lit long after the user stopped — alarming and abnormal
833
+ * ("why is my mic still on?"). A few seconds still coalesces a quick
834
+ * stop→re-record (the iOS re-prompt case) while clearing the mic light almost
835
+ * immediately once the user is actually done. On desktop, re-acquiring after
836
+ * release does NOT re-prompt (the origin's grant persists), so a short window
837
+ * costs nothing there.
838
+ *
839
+ * THE FOUR iOS-SAFARI RECOVERY BRANCHES (`MicInterruptionReason`), each of
840
+ * which used to be a silent audio drop:
841
+ * 1. `ended` — the OS killed the track (lock screen, incoming
842
+ * call, app switch, device unplug). The warm grant
843
+ * is gone; the dead stream is dropped so the next
844
+ * acquire re-acquires (and WILL re-prompt).
845
+ * 2. `muted` — transient interruption (iOS mutes during a
846
+ * call). The grant survives; no re-prompt.
847
+ * 3. `unmuted` — the transient interruption ended.
848
+ * 4. `permission-revoked` — the user/OS pulled the permission; the warm
849
+ * stream is hard-stopped NOW.
850
+ * All four are reported loudly on the interruption channel so a recording
851
+ * surface can react instead of just dropping audio.
852
+ *
853
+ * Reference-counted: concurrent holders (an analyser tap + a recorder) are
854
+ * fine; the stream is only eligible for release when the count hits zero.
855
+ * Callers MUST NOT call `track.stop()` on the returned stream — call
856
+ * `releaseWarmMic()` instead.
857
+ *
858
+ * State lives on `globalThis` under a `Symbol.for` slot (the media-cache
859
+ * pattern) — a dual ESM/CJS module graph must still see ONE mic manager.
860
+ */
861
+ type Listener = (state: MicStreamState) => void;
862
+ type MicInterruptionReason = "ended" | "muted" | "unmuted" | "permission-revoked";
863
+ type InterruptionListener = (reason: MicInterruptionReason) => void;
864
+ type MicStreamState = "idle" | "acquiring" | "active" | "keepalive" | "error";
865
+ /**
866
+ * Subscribe to mic interruptions (track end / mute / permission loss).
867
+ * Returns an unsubscribe fn. Surfaces are expected to make these LOUD — an
868
+ * interruption during a recording is a real event the user must see, not a
869
+ * silent drop.
870
+ */
871
+ declare function subscribeWarmMicInterruption(listener: InterruptionListener): () => void;
872
+ /**
873
+ * Mic-permission revocation → stop the warm stream NOW and emit a loud
874
+ * interruption. In matrx-frontend the ONE canonical permission watcher
875
+ * (audioDevices) calls this; the package, having no host device layer, wires
876
+ * its own minimal Permissions API watcher lazily (`watchMicPermission`) —
877
+ * best-effort only, since Safari's Permissions API may not answer for
878
+ * "microphone" (there, `ended` is the branch that fires instead).
879
+ */
880
+ declare function notifyMicPermissionRevoked(): void;
881
+ /**
882
+ * Set the preferred microphone. Applied as an `{ideal}` deviceId constraint
883
+ * on the NEXT acquire — it does NOT switch a live stream (call
884
+ * `hardStopWarmMic()` then re-acquire to switch mid-session). Pass null to
885
+ * fall back to the system default.
886
+ */
887
+ declare function setPreferredMicDeviceId(id: string | null): void;
888
+ /**
889
+ * The default audio constraints this manager acquires with — exported so the
890
+ * default engine's COMBINED `getUserMedia({video, audio})` prompt (one
891
+ * browser prompt for both permissions instead of camera-then-mic) requests
892
+ * the mic with exactly the same processing + preferred-device shape the
893
+ * singleton itself would use.
894
+ */
895
+ declare function buildWarmMicConstraints(): MediaTrackConstraints;
896
+ /**
897
+ * Adopt an audio stream acquired ELSEWHERE (the default engine's combined
898
+ * camera+mic prompt) as this manager's warm stream, so the grant that
899
+ * combined prompt just earned is banked exactly like one of our own: the
900
+ * next `acquireWarmMic()` inside the keepalive window reuses it with no
901
+ * second prompt. If a warm stream already exists (or one is being acquired),
902
+ * the incoming tracks are stopped immediately — never two live mic streams.
903
+ * With zero holders the adopted stream is parked in the keepalive window, so
904
+ * the mic light clears on the normal schedule.
905
+ */
906
+ declare function adoptWarmAudioStream(stream: MediaStream): void;
907
+ /**
908
+ * Acquire the shared warm mic stream, incrementing the holder count. The
909
+ * returned stream is shared — DO NOT stop its tracks. Call `releaseWarmMic()`
910
+ * when done. If a warm stream exists (active or in its keepalive window) it
911
+ * is reused with no new permission prompt.
912
+ */
913
+ declare function acquireWarmMic(constraints?: MediaTrackConstraints): Promise<MediaStream>;
914
+ /**
915
+ * Release a previously-acquired hold. When the last holder releases, the
916
+ * stream is kept warm for the keepalive window, then actually stopped.
917
+ */
918
+ declare function releaseWarmMic(): void;
919
+ /**
920
+ * Immediately stop the warm stream regardless of keepalive. Use only when
921
+ * the mic must be released NOW; normal teardown uses `releaseWarmMic()`.
922
+ */
923
+ declare function hardStopWarmMic(): void;
924
+ declare function getWarmMicState(): MicStreamState;
925
+ declare function subscribeWarmMic(listener: Listener): () => void;
926
+ /** Diagnostics snapshot for debug panels. */
927
+ declare function warmMicDebug(): {
928
+ state: MicStreamState;
929
+ refCount: number;
930
+ live: boolean;
931
+ keepAliveMs: number;
932
+ };
933
+
934
+ /**
935
+ * CloudLibrarySheet — the recents-thumb destination: where the iPhone opens
936
+ * the camera roll, we open the user's CLOUD media in a full-screen tiled
937
+ * gallery — dark chrome to match the camera, square tiles, newest first.
938
+ *
939
+ * Absorbed from the matrx-frontend host (C22/C23 retrofit, 2026-08-30):
940
+ * the 206-line host sheet was pure chrome over three injectable facts — the
941
+ * item list, the thumbnail renderer, and where a tile navigates. The chrome
942
+ * now ships here; the host injects only data and identity:
943
+ *
944
+ * <CloudLibrarySheet
945
+ * open={open} onClose={close}
946
+ * loading={treeLoading}
947
+ * items={files.map(f => ({ id, fileName, kind, thumbnail: <MyThumb/> }))}
948
+ * onOpenItem={(id) => router.push(`/files/f/${id}`)}
949
+ * onUpload={openPicker} // v3 hosts — upload lives INSIDE the library
950
+ * />
951
+ *
952
+ * Every tile OPENS the real file (no dead ends), and an empty gallery whose
953
+ * only affordance is "close" is a dead end too — with `onUpload` supplied,
954
+ * the empty state offers the one thing a user can do from here.
955
+ */
956
+
957
+ interface CloudLibrarySheetProps {
958
+ open: boolean;
959
+ onClose: () => void;
960
+ /** Newest-first media items. The host supplies the thumbnail node — the
961
+ * sheet never fetches (law 1: no network in this package). */
962
+ items: readonly CaptureCloudLibraryItem[];
963
+ /** True while the host's library data is still loading. */
964
+ loading: boolean;
965
+ /** Opening a tile (host navigation) — a busy overlay renders while true. */
966
+ busy?: boolean;
967
+ /** A tile was tapped. The host opens the REAL file route (no dead ends). */
968
+ onOpenItem: (id: string) => void;
969
+ /**
970
+ * Pick files from the device. Supplied by v3 hosts, where this drawer is
971
+ * the ONLY door to existing media (Arman, 2026-08-30: "we don't need both
972
+ * upload and cloud because we can modify the drawer that has the cloud
973
+ * images to just show an option for uploading").
974
+ *
975
+ * Two controls that both mean "media I already have" is a choice the user
976
+ * should never have been asked to make: whether a file happens to be in
977
+ * the cloud yet is our bookkeeping, not their mental model. Omitted by v2
978
+ * hosts, which still carry a separate UPLOAD lane in the mode row.
979
+ */
980
+ onUpload?: (() => void) | undefined;
981
+ /** Sheet title. Default "Your media". */
982
+ title?: string;
722
983
  }
723
- declare function useDefaultCaptureEngine(options: DefaultEngineOptions): CaptureCameraEngine;
984
+ declare function CloudLibrarySheet({ open, onClose, items, loading, busy, onOpenItem, onUpload, title, }: CloudLibrarySheetProps): React__default.JSX.Element | null;
724
985
 
725
- export { CameraCapture, type CameraCaptureProps, CameraCaptureV3, type CameraCaptureV3Props, CameraFeed, type CaptureAspect, type CaptureCameraEngine, type CaptureCameraMode, type CaptureCameraSlots, type CaptureCameraV3Slots, type CaptureCloudPort, CaptureExpandingField, type CaptureExpandingFieldProps, CaptureFilmstrip, type CaptureFilmstripProps, type CaptureMediaItem, type CaptureMediaSession, type CaptureOptionTile, CaptureRail, type CaptureRailAction, type CaptureRailProps, CaptureSheet, type CaptureSheetAction, type CaptureSheetProps, type CaptureTimerSetting, CountdownOverlay, type DefaultEngineOptions, GridOverlay, HoldShutter, type HoldShutterProps, ImageEditSheet, type ImageEditSheetProps, MediaViewer, type MediaViewerProps, ModeSelector, type ModeSelectorProps, OptionsGridPanel, type OptionsGridPanelProps, type ResolvedMedia, ShutterButton, type ShutterButtonProps, type TrackControls, ZoomRow, type ZoomRowProps, getMediaUrl, invalidateMedia, primeMedia, useDefaultCaptureEngine, useMediaUrl, useTrackControls };
986
+ export { CameraCapture, type CameraCaptureProps, CameraCaptureV3, type CameraCaptureV3Props, CameraFeed, type CaptureAspect, type CaptureCameraEngine, type CaptureCameraMode, type CaptureCameraSlots, type CaptureCameraV3Slots, type CaptureCloudLibraryItem, type CaptureCloudPort, CaptureExpandingField, type CaptureExpandingFieldProps, CaptureFilmstrip, type CaptureFilmstripProps, type CaptureMediaItem, type CaptureMediaSession, type CaptureOptionTile, CaptureRail, type CaptureRailAction, type CaptureRailProps, CaptureSheet, type CaptureSheetAction, type CaptureSheetProps, type CaptureTimerSetting, CloudLibrarySheet, type CloudLibrarySheetProps, CountdownOverlay, type DefaultCaptureEngine, type DefaultEngineOptions, GridOverlay, HoldShutter, type HoldShutterProps, ImageEditSheet, type ImageEditSheetProps, type MediaPermissionState, MediaViewer, type MediaViewerProps, type MicInterruptionReason, type MicStreamState, ModeSelector, type ModeSelectorProps, OptionsGridPanel, type OptionsGridPanelProps, PHOTO_JPEG_QUALITY, type ResolvedMedia, ShutterButton, type ShutterButtonProps, type TrackControls, ZoomRow, type ZoomRowProps, acquireWarmMic, adoptWarmAudioStream, buildWarmMicConstraints, classifyCameraBlockReason, cropBlobToAspect, getMediaUrl, getWarmMicState, hardStopWarmMic, invalidateMedia, isMediaDenialError, nextCameraDevice, notifyMicPermissionRevoked, primeMedia, queryCameraPermission, queryMicPermission, releaseWarmMic, setPreferredMicDeviceId, shouldCombineMicPrompt, subscribeWarmMic, subscribeWarmMicInterruption, useDefaultCaptureEngine, useMediaUrl, useTrackControls, warmMicDebug };
package/dist/react.d.ts CHANGED
@@ -40,6 +40,14 @@ interface CaptureCloudPort {
40
40
  /** Opens the host's cloud media library (tiled gallery). */
41
41
  onOpenLibrary: () => void;
42
42
  }
43
+ /** One media item in the cloud library sheet. The host supplies the
44
+ * thumbnail NODE (its own resolver/auth) — the package never fetches. */
45
+ interface CaptureCloudLibraryItem {
46
+ id: string;
47
+ fileName: string;
48
+ kind: "image" | "video";
49
+ thumbnail: React.ReactNode;
50
+ }
43
51
  /**
44
52
  * The engine port — everything the chrome needs from the host's camera
45
53
  * runtime. The host owns lease acquisition/release, capture and recording;
@@ -709,17 +717,270 @@ interface TrackControls {
709
717
  declare function useTrackControls(stream: MediaStream | null): TrackControls;
710
718
 
711
719
  interface DefaultEngineOptions {
712
- /** Receives every captured photo as a JPEG File. */
720
+ /** Receives every captured photo as a JPEG File (full sensor, aspect crop
721
+ * already applied). */
713
722
  onPhoto: (file: File) => void;
714
723
  /** Receives the finished recording. */
715
724
  onVideo: (file: File, durationMs: number) => void;
716
725
  /** Receives files chosen through the Upload lane. */
717
726
  onFiles: (files: FileList) => void;
718
- /** Record microphone audio with video (asks once, at record time). */
727
+ /** Record microphone audio with video. Default true. With audio on, the
728
+ * engine folds the mic permission into the CAMERA's browser prompt (one
729
+ * combined prompt) whenever a mic prompt would actually appear. */
719
730
  withAudio?: boolean;
731
+ /** The chrome's current mode. In "video" the engine warm-holds the mic so
732
+ * iOS Safari prompts at most once per medium per session. Omitting it
733
+ * skips the warm hold (the record path still acquires the mic). */
734
+ mode?: CaptureCameraMode;
720
735
  facingMode?: "environment" | "user";
721
736
  photoQuality?: number;
737
+ /** Filename prefix for captures (default "capture"). */
738
+ fileNamePrefix?: string;
739
+ /** Loud failure sink (a toast, a logger). Default: console.error. A tapped
740
+ * shutter that produced nothing must say so — never a silent drop. */
741
+ onError?: (message: string, err: unknown) => void;
742
+ }
743
+ /** The engine plus the extras the default path owns (white shutter flash). */
744
+ interface DefaultCaptureEngine extends CaptureCameraEngine {
745
+ /** White shutter-flash flag (120 ms), for the host's flash overlay. */
746
+ flash: boolean;
747
+ /** Take a photo with a non-default filename prefix (e.g. delineators). */
748
+ capturePhotoWith: (opts: {
749
+ fileNamePrefix: string;
750
+ aspect?: CaptureAspect;
751
+ }) => void;
752
+ }
753
+ declare function useDefaultCaptureEngine(options: DefaultEngineOptions): DefaultCaptureEngine;
754
+
755
+ /**
756
+ * src/engine/crop.ts — the ONE aspect-crop implementation.
757
+ *
758
+ * Center-crops a captured JPEG to the requested output aspect. The crop is
759
+ * applied to the FULL sensor frame (honest pixels, not a preview grab) —
760
+ * §2 policy 6 of the chrome: the shutter always captures full-sensor and the
761
+ * selected aspect is a REAL crop of that frame. Ported from the matrx-frontend
762
+ * host adapter (C22 retrofit, 2026-08-30) so every host shares one geometry.
763
+ */
764
+
765
+ declare const PHOTO_JPEG_QUALITY = 0.92;
766
+ declare function cropBlobToAspect(blob: Blob, aspect: CaptureAspect, quality?: number): Promise<Blob>;
767
+ /**
768
+ * Cycle to the next camera in a device list — the flip algorithm the host
769
+ * adapter and the default engine share. Returns null when there is nothing
770
+ * to flip to (fewer than two cameras).
771
+ */
772
+ declare function nextCameraDevice<T extends {
773
+ deviceId: string;
774
+ }>(cameras: readonly T[], currentDeviceId: string | null | undefined): T | null;
775
+
776
+ /**
777
+ * src/engine/permissions.ts — permission pre-checks + error classification
778
+ * for the default engine (and for hosts, so they stop re-implementing the
779
+ * NotAllowedError branch — the C22 rule: quirk branches live HERE).
780
+ */
781
+ type MediaPermissionState = "granted" | "denied" | "prompt" | "unknown";
782
+ /**
783
+ * Pre-check the camera permission WITHOUT prompting. A known-"denied" state
784
+ * lets a surface render its how-to-re-enable explainer instead of hammering
785
+ * getUserMedia with calls that instantly reject (the proven matrx-frontend
786
+ * pre-check contract). "unknown" (Safari) means: just try — the gUM outcome
787
+ * is the truth there.
788
+ */
789
+ declare function queryCameraPermission(): Promise<MediaPermissionState>;
790
+ /** Same pre-check for the microphone — drives the combined-prompt decision. */
791
+ declare function queryMicPermission(): Promise<MediaPermissionState>;
792
+ /**
793
+ * Pure decision for the combined camera+mic prompt: only worth it when a mic
794
+ * prompt would actually appear ("prompt"/"unknown"). "granted" needs no
795
+ * prompt; "denied" would reject the WHOLE combined call and take the camera
796
+ * with it.
797
+ */
798
+ declare function shouldCombineMicPrompt(micPermissionState: MediaPermissionState): boolean;
799
+ /** True when a getUserMedia rejection means the USER/OS denied permission
800
+ * (a missing device is not a denial). */
801
+ declare function isMediaDenialError(err: unknown): boolean;
802
+ /**
803
+ * Classify a camera-acquisition failure into the engine's `blocked` reason.
804
+ * The ONE place the NotAllowedError/SecurityError branch lives — hosts adapt
805
+ * their runtime errors through this instead of re-implementing it.
806
+ */
807
+ declare function classifyCameraBlockReason(err: unknown): "permission-denied" | "not-supported";
808
+
809
+ /**
810
+ * src/engine/warm-mic.ts — the package's warm microphone manager.
811
+ *
812
+ * Ported VERBATIM (2026-08-30, the C22/C23 retrofit) from matrx-frontend's
813
+ * proven `features/audio/micStream.ts` — the module that encodes the real
814
+ * iOS Safari device pain the default engine previously did not have:
815
+ *
816
+ * WHY THIS EXISTS
817
+ * ---------------
818
+ * Every recording surface used to call `navigator.mediaDevices.getUserMedia`
819
+ * itself and fully `track.stop()` the stream when it finished. On mobile
820
+ * (notably iOS Safari) that produces a permission/"in use" prompt on *every*
821
+ * single recording, because the previous grant is torn down before the next
822
+ * acquisition. Tapping record three times = three prompts.
823
+ *
824
+ * This manager keeps ONE stream warm and hands it to every caller. After the
825
+ * last holder releases it, the stream is kept alive for a SHORT keepalive
826
+ * window; if another recording starts within that window, the same live
827
+ * stream (and the same OS grant) is reused — no second prompt. Only after the
828
+ * window elapses with no holders is the stream actually stopped (mic light
829
+ * off), so we don't hold the mic open forever.
830
+ *
831
+ * KEEPALIVE IS DELIBERATELY SHORT (a few seconds). Minutes keep the browser's
832
+ * recording indicator lit long after the user stopped — alarming and abnormal
833
+ * ("why is my mic still on?"). A few seconds still coalesces a quick
834
+ * stop→re-record (the iOS re-prompt case) while clearing the mic light almost
835
+ * immediately once the user is actually done. On desktop, re-acquiring after
836
+ * release does NOT re-prompt (the origin's grant persists), so a short window
837
+ * costs nothing there.
838
+ *
839
+ * THE FOUR iOS-SAFARI RECOVERY BRANCHES (`MicInterruptionReason`), each of
840
+ * which used to be a silent audio drop:
841
+ * 1. `ended` — the OS killed the track (lock screen, incoming
842
+ * call, app switch, device unplug). The warm grant
843
+ * is gone; the dead stream is dropped so the next
844
+ * acquire re-acquires (and WILL re-prompt).
845
+ * 2. `muted` — transient interruption (iOS mutes during a
846
+ * call). The grant survives; no re-prompt.
847
+ * 3. `unmuted` — the transient interruption ended.
848
+ * 4. `permission-revoked` — the user/OS pulled the permission; the warm
849
+ * stream is hard-stopped NOW.
850
+ * All four are reported loudly on the interruption channel so a recording
851
+ * surface can react instead of just dropping audio.
852
+ *
853
+ * Reference-counted: concurrent holders (an analyser tap + a recorder) are
854
+ * fine; the stream is only eligible for release when the count hits zero.
855
+ * Callers MUST NOT call `track.stop()` on the returned stream — call
856
+ * `releaseWarmMic()` instead.
857
+ *
858
+ * State lives on `globalThis` under a `Symbol.for` slot (the media-cache
859
+ * pattern) — a dual ESM/CJS module graph must still see ONE mic manager.
860
+ */
861
+ type Listener = (state: MicStreamState) => void;
862
+ type MicInterruptionReason = "ended" | "muted" | "unmuted" | "permission-revoked";
863
+ type InterruptionListener = (reason: MicInterruptionReason) => void;
864
+ type MicStreamState = "idle" | "acquiring" | "active" | "keepalive" | "error";
865
+ /**
866
+ * Subscribe to mic interruptions (track end / mute / permission loss).
867
+ * Returns an unsubscribe fn. Surfaces are expected to make these LOUD — an
868
+ * interruption during a recording is a real event the user must see, not a
869
+ * silent drop.
870
+ */
871
+ declare function subscribeWarmMicInterruption(listener: InterruptionListener): () => void;
872
+ /**
873
+ * Mic-permission revocation → stop the warm stream NOW and emit a loud
874
+ * interruption. In matrx-frontend the ONE canonical permission watcher
875
+ * (audioDevices) calls this; the package, having no host device layer, wires
876
+ * its own minimal Permissions API watcher lazily (`watchMicPermission`) —
877
+ * best-effort only, since Safari's Permissions API may not answer for
878
+ * "microphone" (there, `ended` is the branch that fires instead).
879
+ */
880
+ declare function notifyMicPermissionRevoked(): void;
881
+ /**
882
+ * Set the preferred microphone. Applied as an `{ideal}` deviceId constraint
883
+ * on the NEXT acquire — it does NOT switch a live stream (call
884
+ * `hardStopWarmMic()` then re-acquire to switch mid-session). Pass null to
885
+ * fall back to the system default.
886
+ */
887
+ declare function setPreferredMicDeviceId(id: string | null): void;
888
+ /**
889
+ * The default audio constraints this manager acquires with — exported so the
890
+ * default engine's COMBINED `getUserMedia({video, audio})` prompt (one
891
+ * browser prompt for both permissions instead of camera-then-mic) requests
892
+ * the mic with exactly the same processing + preferred-device shape the
893
+ * singleton itself would use.
894
+ */
895
+ declare function buildWarmMicConstraints(): MediaTrackConstraints;
896
+ /**
897
+ * Adopt an audio stream acquired ELSEWHERE (the default engine's combined
898
+ * camera+mic prompt) as this manager's warm stream, so the grant that
899
+ * combined prompt just earned is banked exactly like one of our own: the
900
+ * next `acquireWarmMic()` inside the keepalive window reuses it with no
901
+ * second prompt. If a warm stream already exists (or one is being acquired),
902
+ * the incoming tracks are stopped immediately — never two live mic streams.
903
+ * With zero holders the adopted stream is parked in the keepalive window, so
904
+ * the mic light clears on the normal schedule.
905
+ */
906
+ declare function adoptWarmAudioStream(stream: MediaStream): void;
907
+ /**
908
+ * Acquire the shared warm mic stream, incrementing the holder count. The
909
+ * returned stream is shared — DO NOT stop its tracks. Call `releaseWarmMic()`
910
+ * when done. If a warm stream exists (active or in its keepalive window) it
911
+ * is reused with no new permission prompt.
912
+ */
913
+ declare function acquireWarmMic(constraints?: MediaTrackConstraints): Promise<MediaStream>;
914
+ /**
915
+ * Release a previously-acquired hold. When the last holder releases, the
916
+ * stream is kept warm for the keepalive window, then actually stopped.
917
+ */
918
+ declare function releaseWarmMic(): void;
919
+ /**
920
+ * Immediately stop the warm stream regardless of keepalive. Use only when
921
+ * the mic must be released NOW; normal teardown uses `releaseWarmMic()`.
922
+ */
923
+ declare function hardStopWarmMic(): void;
924
+ declare function getWarmMicState(): MicStreamState;
925
+ declare function subscribeWarmMic(listener: Listener): () => void;
926
+ /** Diagnostics snapshot for debug panels. */
927
+ declare function warmMicDebug(): {
928
+ state: MicStreamState;
929
+ refCount: number;
930
+ live: boolean;
931
+ keepAliveMs: number;
932
+ };
933
+
934
+ /**
935
+ * CloudLibrarySheet — the recents-thumb destination: where the iPhone opens
936
+ * the camera roll, we open the user's CLOUD media in a full-screen tiled
937
+ * gallery — dark chrome to match the camera, square tiles, newest first.
938
+ *
939
+ * Absorbed from the matrx-frontend host (C22/C23 retrofit, 2026-08-30):
940
+ * the 206-line host sheet was pure chrome over three injectable facts — the
941
+ * item list, the thumbnail renderer, and where a tile navigates. The chrome
942
+ * now ships here; the host injects only data and identity:
943
+ *
944
+ * <CloudLibrarySheet
945
+ * open={open} onClose={close}
946
+ * loading={treeLoading}
947
+ * items={files.map(f => ({ id, fileName, kind, thumbnail: <MyThumb/> }))}
948
+ * onOpenItem={(id) => router.push(`/files/f/${id}`)}
949
+ * onUpload={openPicker} // v3 hosts — upload lives INSIDE the library
950
+ * />
951
+ *
952
+ * Every tile OPENS the real file (no dead ends), and an empty gallery whose
953
+ * only affordance is "close" is a dead end too — with `onUpload` supplied,
954
+ * the empty state offers the one thing a user can do from here.
955
+ */
956
+
957
+ interface CloudLibrarySheetProps {
958
+ open: boolean;
959
+ onClose: () => void;
960
+ /** Newest-first media items. The host supplies the thumbnail node — the
961
+ * sheet never fetches (law 1: no network in this package). */
962
+ items: readonly CaptureCloudLibraryItem[];
963
+ /** True while the host's library data is still loading. */
964
+ loading: boolean;
965
+ /** Opening a tile (host navigation) — a busy overlay renders while true. */
966
+ busy?: boolean;
967
+ /** A tile was tapped. The host opens the REAL file route (no dead ends). */
968
+ onOpenItem: (id: string) => void;
969
+ /**
970
+ * Pick files from the device. Supplied by v3 hosts, where this drawer is
971
+ * the ONLY door to existing media (Arman, 2026-08-30: "we don't need both
972
+ * upload and cloud because we can modify the drawer that has the cloud
973
+ * images to just show an option for uploading").
974
+ *
975
+ * Two controls that both mean "media I already have" is a choice the user
976
+ * should never have been asked to make: whether a file happens to be in
977
+ * the cloud yet is our bookkeeping, not their mental model. Omitted by v2
978
+ * hosts, which still carry a separate UPLOAD lane in the mode row.
979
+ */
980
+ onUpload?: (() => void) | undefined;
981
+ /** Sheet title. Default "Your media". */
982
+ title?: string;
722
983
  }
723
- declare function useDefaultCaptureEngine(options: DefaultEngineOptions): CaptureCameraEngine;
984
+ declare function CloudLibrarySheet({ open, onClose, items, loading, busy, onOpenItem, onUpload, title, }: CloudLibrarySheetProps): React__default.JSX.Element | null;
724
985
 
725
- export { CameraCapture, type CameraCaptureProps, CameraCaptureV3, type CameraCaptureV3Props, CameraFeed, type CaptureAspect, type CaptureCameraEngine, type CaptureCameraMode, type CaptureCameraSlots, type CaptureCameraV3Slots, type CaptureCloudPort, CaptureExpandingField, type CaptureExpandingFieldProps, CaptureFilmstrip, type CaptureFilmstripProps, type CaptureMediaItem, type CaptureMediaSession, type CaptureOptionTile, CaptureRail, type CaptureRailAction, type CaptureRailProps, CaptureSheet, type CaptureSheetAction, type CaptureSheetProps, type CaptureTimerSetting, CountdownOverlay, type DefaultEngineOptions, GridOverlay, HoldShutter, type HoldShutterProps, ImageEditSheet, type ImageEditSheetProps, MediaViewer, type MediaViewerProps, ModeSelector, type ModeSelectorProps, OptionsGridPanel, type OptionsGridPanelProps, type ResolvedMedia, ShutterButton, type ShutterButtonProps, type TrackControls, ZoomRow, type ZoomRowProps, getMediaUrl, invalidateMedia, primeMedia, useDefaultCaptureEngine, useMediaUrl, useTrackControls };
986
+ export { CameraCapture, type CameraCaptureProps, CameraCaptureV3, type CameraCaptureV3Props, CameraFeed, type CaptureAspect, type CaptureCameraEngine, type CaptureCameraMode, type CaptureCameraSlots, type CaptureCameraV3Slots, type CaptureCloudLibraryItem, type CaptureCloudPort, CaptureExpandingField, type CaptureExpandingFieldProps, CaptureFilmstrip, type CaptureFilmstripProps, type CaptureMediaItem, type CaptureMediaSession, type CaptureOptionTile, CaptureRail, type CaptureRailAction, type CaptureRailProps, CaptureSheet, type CaptureSheetAction, type CaptureSheetProps, type CaptureTimerSetting, CloudLibrarySheet, type CloudLibrarySheetProps, CountdownOverlay, type DefaultCaptureEngine, type DefaultEngineOptions, GridOverlay, HoldShutter, type HoldShutterProps, ImageEditSheet, type ImageEditSheetProps, type MediaPermissionState, MediaViewer, type MediaViewerProps, type MicInterruptionReason, type MicStreamState, ModeSelector, type ModeSelectorProps, OptionsGridPanel, type OptionsGridPanelProps, PHOTO_JPEG_QUALITY, type ResolvedMedia, ShutterButton, type ShutterButtonProps, type TrackControls, ZoomRow, type ZoomRowProps, acquireWarmMic, adoptWarmAudioStream, buildWarmMicConstraints, classifyCameraBlockReason, cropBlobToAspect, getMediaUrl, getWarmMicState, hardStopWarmMic, invalidateMedia, isMediaDenialError, nextCameraDevice, notifyMicPermissionRevoked, primeMedia, queryCameraPermission, queryMicPermission, releaseWarmMic, setPreferredMicDeviceId, shouldCombineMicPrompt, subscribeWarmMic, subscribeWarmMicInterruption, useDefaultCaptureEngine, useMediaUrl, useTrackControls, warmMicDebug };