/** * useContentRotation — returns a CSS transform that rotates control * content so labels stay upright relative to device gravity, * regardless of whether the OS rotated the framebuffer. * * ## Why this exists * * v0.12 anchored ``'s bottom controls to the home-indicator * edge so they stay in thumb reach on phones in landscape on * non-locked iOS hosts. The anchoring works because the OS rotates * the framebuffer to match the device, so a JS-bottom view in * landscape is the device's actual landscape-bottom edge. * * On locked-portrait hosts (the most common production * configuration) the OS does NOT rotate the framebuffer when the * device tilts to landscape. v0.12 still anchored controls to * "JS-bottom" — which is now the device's side edge — so the * shutter sits where the thumb expects, BUT the labels inside * each control (`AR`, `1×`, `0.5×`, the lens chip pills, the gear) * render at their JS-portrait baseline, so the user holding the * device sideways reads them at 90°. * * This hook fixes that by applying a `transform: rotate(±90°)` to * the control's *content* so it appears upright relative to actual * gravity, while the control container itself stays in place. * * ## How the rotation is computed * * Two signals: * - **Framebuffer rotation** — what rotation has the OS already * applied to the JS layout? Read from * `useWindowDimensions().width > height` — non-locked + * device-landscape is the only case where the OS rotates, * and that's exactly when `jsLandscape === true`. * - **Device-physical rotation** — what rotation does the device * have relative to gravity? Read from `useDeviceOrientation()` * (accelerometer-derived). * * The content rotation we apply is the *difference* between * device-physical and framebuffer rotation, so the net rotation * (content × framebuffer) equals device-physical → labels are * upright in the world. * * ## Truth table * * | Host config | Device | jsLandscape | Net rot | * |--- |--- |--- |--- | * | Locked-portrait | portrait | false | 0° | * | Locked-portrait | landscape-left | false | 90° | * | Locked-portrait | landscape-right | false | -90° | * | Locked-portrait | upside-down | false | 180° | * | Non-locked | portrait | false | 0° | * | Non-locked | landscape-left | true | 0° | * | Non-locked | landscape-right | true | 0° | * * The 0° case is the common one (locked-portrait + device-portrait * OR non-locked + framebuffer-already-rotated); we return an empty * style object so React skips the layout work. * * ## Caveats * * - Rotation transforms preserve hit-testing in RN 0.84 (verified * on iOS + Android), but historical RN versions had bugs in this * area. If support for older RN is added, retest pressables. * - Containers whose sized layouts depend on un-rotated content * (e.g. a 100px-wide pill containing text that's now rotated 90°) * may overflow. Fixed-size pills (the lens chip, AR toggle, * flash button) are fine; the header title's `flex: 1 + textAlign: * center` may need tuning when rotated — see `CaptureHeader`'s * own rotation handling. */ import { type ViewStyle } from 'react-native'; import { type DeviceOrientation } from './useDeviceOrientation'; /** * Measured JS-layout orientation provided by ``. * * `useWindowDimensions()` freezes at its open-time value inside an iOS * RN `Modal` (any presentationStyle): the modal rotates but RN's global * window-dimension state never receives a change event, so hosts that * present `` in a modal would get a stale `jsLandscape` and the * bottom controls would anchor to the wrong edge after rotation. * * `` therefore measures its own root view via `onLayout` (which * IS reliable inside modals) and provides `width > height` here. The * hook below prefers this context and only falls back to * `useWindowDimensions` when rendered outside a `` tree. */ export declare const HostJsLandscapeContext: import("react").Context; export type ContentRotationDeg = 0 | 90 | -90 | 180; /** * Return type for `useContentRotation`. Typed structurally on just * the `transform` property so it spreads cleanly into ViewStyle, * TextStyle, AND ImageStyle — all three accept identical transform * shapes in RN 0.84. Returning the more specific `ViewStyle` would * collide with ImageStyle's stricter `overflow` enum at * call sites. */ export type ContentRotationStyle = { transform?: ViewStyle['transform']; }; /** * Pure rotation computation. Exported so tests can exercise the * full truth table without booting a React render. */ export declare function contentRotationDeg(jsLandscape: boolean, deviceOrient: DeviceOrientation): ContentRotationDeg; /** * Returns the rotation as a ready-to-spread style object. Empty * object in the common 0° case so React skips the layout work. * Type `ContentRotationStyle` is structurally just `{ transform? }` * so call sites can spread it into ViewStyle, TextStyle, or * ImageStyle interchangeably. */ export declare function useContentRotation(): ContentRotationStyle; /** * A user-perceived screen edge — the side of the device the operator sees, * independent of how the OS laid out the framebuffer. */ export type UserEdge = 'top' | 'bottom' | 'left' | 'right'; /** * Placement for a full-screen HUD layer: the flex anchor (applied to the * layer) plus the rotation (applied to the pill inside it). */ export interface EdgePlacement { /** Flex alignment for a `StyleSheet.absoluteFill` layer. */ container: ViewStyle; /** Content rotation, e.g. `'90deg'`, that reads upright to the user. */ rotate: string; } /** * Rotate a USER edge into the FRAMEBUFFER edge that currently sits at it. * * The framebuffer is rotated relative to the user by exactly `deg` * (= {@link contentRotationDeg}: `deviceRot − framebufferRot`). So the * framebuffer edge coinciding with a given user edge is that edge stepped * clockwise by `deg`. Calibrated against the existing per-orientation * placement: e.g. a user-`bottom` counter in locked-portrait landscape-left * (deg=90) → `bottom` +1 step → `left` = `alignItems:'flex-start'`, exactly * what the old `topCenterForOrientation` produced. */ export declare function framebufferEdge(userEdge: UserEdge, deg: ContentRotationDeg): UserEdge; /** * Pure placement: pin a HUD pill to a USER-perceived edge, upright, on ANY * host configuration. Single source of truth combining the net content * rotation ({@link contentRotationDeg}) with the framebuffer anchor that maps * to `userEdge`. * * Correct on BOTH host configs from one derivation: * - locked-portrait (jsLandscape always false): reproduces the classic * per-orientation anchor+rotate (deviceRot, framebuffer edge = userEdge * stepped by deviceRot); * - non-locked (jsLandscape true in landscape): net rotation collapses to * 0° and the anchor stays at the user edge, because the OS already * rotated the framebuffer to match the device. * * `inset` is the gap from that edge (px). */ export declare function placeAtUserEdge(deviceOrient: DeviceOrientation, jsLandscape: boolean, userEdge: UserEdge, inset: number): EdgePlacement; /** * Hook form of {@link placeAtUserEdge}: reads the physical device orientation * and the measured jsLandscape (preferring ``'s * {@link HostJsLandscapeContext}, since `useWindowDimensions` freezes inside * iOS modals). HUD overlays call this instead of hand-rolling their own * orientation switch, so they can never disagree. */ export declare function useEdgePlacement(userEdge: UserEdge, inset: number): EdgePlacement; //# sourceMappingURL=useContentRotation.d.ts.map