@ai-matrx/capture 0.2.0 → 0.3.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.cjs CHANGED
@@ -22,11 +22,15 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
22
22
  var react_exports = {};
23
23
  __export(react_exports, {
24
24
  CameraCapture: () => CameraCapture,
25
+ CameraCaptureV3: () => CameraCaptureV3,
25
26
  CameraFeed: () => CameraFeed,
27
+ CaptureExpandingField: () => CaptureExpandingField,
26
28
  CaptureFilmstrip: () => CaptureFilmstrip,
29
+ CaptureRail: () => CaptureRail,
27
30
  CaptureSheet: () => CaptureSheet,
28
31
  CountdownOverlay: () => CountdownOverlay,
29
32
  GridOverlay: () => GridOverlay,
33
+ HoldShutter: () => HoldShutter,
30
34
  ImageEditSheet: () => ImageEditSheet,
31
35
  MediaViewer: () => MediaViewer,
32
36
  ModeSelector: () => ModeSelector,
@@ -1292,6 +1296,7 @@ function CameraCapture({
1292
1296
  onModeChange,
1293
1297
  preview,
1294
1298
  media,
1299
+ onReviewOpenChange,
1295
1300
  onClose,
1296
1301
  blockedSheet,
1297
1302
  controlsHidden = false,
@@ -1309,6 +1314,10 @@ function CameraCapture({
1309
1314
  const [blockedDismissed, setBlockedDismissed] = (0, import_react5.useState)(false);
1310
1315
  const countdownRef = (0, import_react5.useRef)(null);
1311
1316
  const controls = useTrackControls(engine.stream);
1317
+ const reviewOpen = viewerKey !== null || editTarget !== null;
1318
+ (0, import_react5.useEffect)(() => {
1319
+ onReviewOpenChange?.(reviewOpen);
1320
+ }, [reviewOpen, onReviewOpenChange]);
1312
1321
  const clearCountdown = (0, import_react5.useCallback)(() => {
1313
1322
  if (countdownRef.current) clearInterval(countdownRef.current);
1314
1323
  countdownRef.current = null;
@@ -1638,15 +1647,665 @@ function CameraCapture({
1638
1647
  ] });
1639
1648
  }
1640
1649
 
1641
- // src/components/CameraFeed.tsx
1650
+ // src/components/CameraCaptureV3.tsx
1651
+ var import_react8 = require("react");
1652
+ var import_lucide_react5 = require("lucide-react");
1653
+
1654
+ // src/components/CaptureRail.tsx
1642
1655
  var import_react6 = require("react");
1643
1656
  var import_jsx_runtime12 = require("react/jsx-runtime");
1657
+ function CaptureRail({
1658
+ actions,
1659
+ defaultExpanded = false,
1660
+ className
1661
+ }) {
1662
+ const [expanded, setExpanded] = (0, import_react6.useState)(defaultExpanded);
1663
+ const primary = actions.filter((a) => a.primary);
1664
+ const extra = actions.filter((a) => !a.primary);
1665
+ const shown = primary.length === 0 ? actions : expanded ? actions : primary;
1666
+ const collapsible = primary.length > 0 && extra.length > 0;
1667
+ if (actions.length === 0) return null;
1668
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
1669
+ "div",
1670
+ {
1671
+ className: cn(
1672
+ "pointer-events-auto flex flex-col items-center gap-1",
1673
+ className
1674
+ ),
1675
+ children: [
1676
+ shown.map((action) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(RailButton, { action }, action.id)),
1677
+ collapsible ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1678
+ "button",
1679
+ {
1680
+ type: "button",
1681
+ onClick: () => setExpanded((open) => !open),
1682
+ "aria-expanded": expanded,
1683
+ "aria-label": expanded ? "Fewer options" : "More options",
1684
+ className: cn(
1685
+ "mt-0.5 flex h-9 w-9 touch-manipulation items-center justify-center",
1686
+ "rounded-full text-white/90 transition-colors hover:bg-white/10"
1687
+ ),
1688
+ children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1689
+ Chevron,
1690
+ {
1691
+ className: cn(
1692
+ "h-5 w-5 transition-transform duration-200",
1693
+ expanded && "rotate-180"
1694
+ )
1695
+ }
1696
+ )
1697
+ }
1698
+ ) : null
1699
+ ]
1700
+ }
1701
+ );
1702
+ }
1703
+ function RailButton({ action }) {
1704
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
1705
+ "button",
1706
+ {
1707
+ type: "button",
1708
+ onClick: action.onPress,
1709
+ disabled: action.disabled,
1710
+ "aria-label": action.label,
1711
+ "aria-pressed": action.active ?? void 0,
1712
+ className: cn(
1713
+ "flex w-12 shrink-0 touch-manipulation flex-col items-center justify-center gap-0.5",
1714
+ "rounded-2xl py-1.5 transition-colors",
1715
+ action.active ? "text-[#FFCC00]" : "text-white",
1716
+ action.disabled ? "opacity-40" : "hover:bg-white/10 active:bg-white/15"
1717
+ ),
1718
+ children: [
1719
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "drop-shadow-[0_1px_3px_rgba(0,0,0,0.6)]", children: action.icon }),
1720
+ action.valueLabel ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "text-[10px] font-semibold leading-none tabular-nums drop-shadow-[0_1px_2px_rgba(0,0,0,0.7)]", children: action.valueLabel }) : null
1721
+ ]
1722
+ }
1723
+ );
1724
+ }
1725
+ function Chevron({ className }) {
1726
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1727
+ "svg",
1728
+ {
1729
+ viewBox: "0 0 24 24",
1730
+ fill: "none",
1731
+ stroke: "currentColor",
1732
+ strokeWidth: "2.5",
1733
+ strokeLinecap: "round",
1734
+ strokeLinejoin: "round",
1735
+ className,
1736
+ "aria-hidden": "true",
1737
+ children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("path", { d: "m6 9 6 6 6-6" })
1738
+ }
1739
+ );
1740
+ }
1741
+
1742
+ // src/components/HoldShutter.tsx
1743
+ var import_react7 = require("react");
1744
+ var import_jsx_runtime13 = require("react/jsx-runtime");
1745
+ var HOLD_MS = 320;
1746
+ var LOCK_AFTER_MS = 1200;
1747
+ function HoldShutter({
1748
+ recording,
1749
+ elapsedSeconds,
1750
+ maxSeconds,
1751
+ disabled = false,
1752
+ onPressStart,
1753
+ onPhoto,
1754
+ onStartRecording,
1755
+ onStopRecording
1756
+ }) {
1757
+ const holdTimer = (0, import_react7.useRef)(null);
1758
+ const startedRecording = (0, import_react7.useRef)(false);
1759
+ const [pressed, setPressed] = (0, import_react7.useState)(false);
1760
+ const [locked, setLocked] = (0, import_react7.useState)(false);
1761
+ const clearHold = (0, import_react7.useCallback)(() => {
1762
+ if (holdTimer.current !== null) {
1763
+ clearTimeout(holdTimer.current);
1764
+ holdTimer.current = null;
1765
+ }
1766
+ }, []);
1767
+ (0, import_react7.useEffect)(() => {
1768
+ if (!recording) {
1769
+ setLocked(false);
1770
+ return;
1771
+ }
1772
+ if (elapsedSeconds * 1e3 >= LOCK_AFTER_MS) setLocked(true);
1773
+ }, [recording, elapsedSeconds]);
1774
+ (0, import_react7.useEffect)(() => clearHold, [clearHold]);
1775
+ const onPointerDown = (0, import_react7.useCallback)(
1776
+ (event) => {
1777
+ if (disabled) return;
1778
+ event.currentTarget.setPointerCapture?.(event.pointerId);
1779
+ setPressed(true);
1780
+ onPressStart?.();
1781
+ if (recording && locked) return;
1782
+ startedRecording.current = false;
1783
+ holdTimer.current = setTimeout(() => {
1784
+ holdTimer.current = null;
1785
+ startedRecording.current = true;
1786
+ onStartRecording();
1787
+ }, HOLD_MS);
1788
+ },
1789
+ [disabled, locked, onPressStart, onStartRecording, recording]
1790
+ );
1791
+ const endPress = (0, import_react7.useCallback)(() => {
1792
+ if (!pressed) return;
1793
+ setPressed(false);
1794
+ const wasHold = startedRecording.current;
1795
+ clearHold();
1796
+ startedRecording.current = false;
1797
+ if (recording && locked && !wasHold) {
1798
+ onStopRecording();
1799
+ return;
1800
+ }
1801
+ if (wasHold) {
1802
+ if (!locked) onStopRecording();
1803
+ return;
1804
+ }
1805
+ if (!recording) onPhoto();
1806
+ }, [clearHold, locked, onPhoto, onStopRecording, pressed, recording]);
1807
+ const progress = maxSeconds && maxSeconds > 0 ? Math.min(1, elapsedSeconds / maxSeconds) : 0;
1808
+ const R = 39;
1809
+ const C = 2 * Math.PI * R;
1810
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "relative flex h-[86px] w-[86px] shrink-0 items-center justify-center", children: [
1811
+ recording && maxSeconds ? /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
1812
+ "svg",
1813
+ {
1814
+ className: "pointer-events-none absolute inset-0 -rotate-90",
1815
+ viewBox: "0 0 86 86",
1816
+ "aria-hidden": "true",
1817
+ children: [
1818
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1819
+ "circle",
1820
+ {
1821
+ cx: "43",
1822
+ cy: "43",
1823
+ r: R,
1824
+ fill: "none",
1825
+ stroke: "rgba(255,255,255,0.25)",
1826
+ strokeWidth: "4"
1827
+ }
1828
+ ),
1829
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1830
+ "circle",
1831
+ {
1832
+ cx: "43",
1833
+ cy: "43",
1834
+ r: R,
1835
+ fill: "none",
1836
+ stroke: "#FF3B30",
1837
+ strokeWidth: "4",
1838
+ strokeLinecap: "round",
1839
+ strokeDasharray: C,
1840
+ strokeDashoffset: C * (1 - progress)
1841
+ }
1842
+ )
1843
+ ]
1844
+ }
1845
+ ) : null,
1846
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1847
+ "button",
1848
+ {
1849
+ type: "button",
1850
+ disabled,
1851
+ onPointerDown,
1852
+ onPointerUp: endPress,
1853
+ onPointerCancel: endPress,
1854
+ onLostPointerCapture: endPress,
1855
+ onContextMenu: (e) => e.preventDefault(),
1856
+ "aria-label": recording ? locked ? "Stop recording" : "Recording \u2014 release to stop" : "Tap for a photo, hold to record",
1857
+ className: cn(
1858
+ "group flex h-[74px] w-[74px] shrink-0 touch-none select-none items-center justify-center",
1859
+ "rounded-full border-[3.5px] border-white transition-opacity",
1860
+ disabled && "opacity-30"
1861
+ ),
1862
+ children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1863
+ "span",
1864
+ {
1865
+ className: cn(
1866
+ "block transition-all duration-200 ease-out",
1867
+ recording ? "h-7 w-7 rounded-[6px] bg-[#FF3B30]" : cn(
1868
+ "h-[62px] w-[62px] rounded-full bg-white",
1869
+ pressed && "scale-90"
1870
+ )
1871
+ )
1872
+ }
1873
+ )
1874
+ }
1875
+ ),
1876
+ recording ? /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
1877
+ "span",
1878
+ {
1879
+ className: cn(
1880
+ "pointer-events-none absolute -bottom-1 left-1/2 -translate-x-1/2 translate-y-full",
1881
+ "rounded-full bg-black/60 px-2 py-0.5 text-[11px] font-medium tabular-nums text-white"
1882
+ ),
1883
+ children: [
1884
+ formatElapsed2(elapsedSeconds),
1885
+ locked ? " \xB7 tap to stop" : ""
1886
+ ]
1887
+ }
1888
+ ) : null
1889
+ ] });
1890
+ }
1891
+ function formatElapsed2(totalSeconds) {
1892
+ const m = Math.floor(totalSeconds / 60);
1893
+ const s = String(Math.floor(totalSeconds % 60)).padStart(2, "0");
1894
+ return `${m}:${s}`;
1895
+ }
1896
+
1897
+ // src/components/CameraCaptureV3.tsx
1898
+ var import_jsx_runtime14 = require("react/jsx-runtime");
1899
+ var ASPECT_CYCLE2 = ["full", "4:3", "1:1", "16:9"];
1900
+ function CameraCaptureV3({
1901
+ engine,
1902
+ cloud,
1903
+ preview,
1904
+ media,
1905
+ onRecordIntent,
1906
+ maxRecordSeconds,
1907
+ onReviewOpenChange,
1908
+ onClose,
1909
+ blockedSheet,
1910
+ controlsHidden = false,
1911
+ shutterDisabled = false,
1912
+ slots = {}
1913
+ }) {
1914
+ const [viewerKey, setViewerKey] = (0, import_react8.useState)(null);
1915
+ const [editTarget, setEditTarget] = (0, import_react8.useState)(
1916
+ null
1917
+ );
1918
+ const [gridOn, setGridOn] = (0, import_react8.useState)(false);
1919
+ const [timerSetting, setTimerSetting] = (0, import_react8.useState)(0);
1920
+ const [aspect, setAspect] = (0, import_react8.useState)("full");
1921
+ const [countdown, setCountdown] = (0, import_react8.useState)(null);
1922
+ const [blockedDismissed, setBlockedDismissed] = (0, import_react8.useState)(false);
1923
+ const countdownRef = (0, import_react8.useRef)(null);
1924
+ const controls = useTrackControls(engine.stream);
1925
+ const reviewOpen = viewerKey !== null || editTarget !== null;
1926
+ (0, import_react8.useEffect)(() => {
1927
+ onReviewOpenChange?.(reviewOpen);
1928
+ }, [reviewOpen, onReviewOpenChange]);
1929
+ const clearCountdown = (0, import_react8.useCallback)(() => {
1930
+ if (countdownRef.current) clearInterval(countdownRef.current);
1931
+ countdownRef.current = null;
1932
+ setCountdown(null);
1933
+ }, []);
1934
+ (0, import_react8.useEffect)(() => clearCountdown, [clearCountdown]);
1935
+ (0, import_react8.useEffect)(() => {
1936
+ if (engine.recording) clearCountdown();
1937
+ }, [engine.recording, clearCountdown]);
1938
+ const onPhoto = (0, import_react8.useCallback)(() => {
1939
+ if (countdown !== null) {
1940
+ clearCountdown();
1941
+ return;
1942
+ }
1943
+ if (timerSetting === 0) {
1944
+ engine.onCapturePhoto({ aspect });
1945
+ return;
1946
+ }
1947
+ let remaining = timerSetting;
1948
+ setCountdown(remaining);
1949
+ countdownRef.current = setInterval(() => {
1950
+ remaining -= 1;
1951
+ if (remaining <= 0) {
1952
+ clearCountdown();
1953
+ engine.onCapturePhoto({ aspect });
1954
+ } else {
1955
+ setCountdown(remaining);
1956
+ }
1957
+ }, 1e3);
1958
+ }, [aspect, clearCountdown, countdown, engine, timerSetting]);
1959
+ const blocked = engine.blocked !== null;
1960
+ const coreRail = [
1961
+ ...engine.onFlipCamera ? [
1962
+ {
1963
+ id: "flip",
1964
+ label: "Switch camera",
1965
+ icon: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react5.RefreshCw, { className: "h-[22px] w-[22px]" }),
1966
+ primary: true,
1967
+ onPress: engine.onFlipCamera
1968
+ }
1969
+ ] : [],
1970
+ ...controls.torchSupported ? [
1971
+ {
1972
+ id: "flash",
1973
+ label: "Flash",
1974
+ icon: controls.torchOn ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react5.Zap, { className: "h-[22px] w-[22px]", fill: "currentColor" }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react5.ZapOff, { className: "h-[22px] w-[22px]" }),
1975
+ active: controls.torchOn,
1976
+ primary: true,
1977
+ onPress: controls.toggleTorch
1978
+ }
1979
+ ] : [],
1980
+ {
1981
+ id: "timer",
1982
+ label: "Timer",
1983
+ icon: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react5.Timer, { className: "h-[22px] w-[22px]" }),
1984
+ active: timerSetting !== 0,
1985
+ valueLabel: timerSetting === 0 ? void 0 : `${timerSetting}s`,
1986
+ onPress: () => setTimerSetting((t) => t === 0 ? 3 : t === 3 ? 10 : 0)
1987
+ },
1988
+ {
1989
+ id: "grid",
1990
+ label: "Grid",
1991
+ icon: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react5.Grid3x3, { className: "h-[22px] w-[22px]" }),
1992
+ active: gridOn,
1993
+ onPress: () => setGridOn((g) => !g)
1994
+ },
1995
+ {
1996
+ id: "aspect",
1997
+ label: "Aspect",
1998
+ icon: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react5.Proportions, { className: "h-[22px] w-[22px]" }),
1999
+ active: aspect !== "full",
2000
+ valueLabel: aspect === "full" ? void 0 : aspect,
2001
+ onPress: () => setAspect(
2002
+ (a) => ASPECT_CYCLE2[(ASPECT_CYCLE2.indexOf(a) + 1) % ASPECT_CYCLE2.length] ?? "full"
2003
+ )
2004
+ },
2005
+ ...controls.exposureSupported ? [
2006
+ {
2007
+ id: "exposure",
2008
+ label: "Exposure",
2009
+ icon: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react5.SunMedium, { className: "h-[22px] w-[22px]" }),
2010
+ active: controls.exposure !== 0,
2011
+ valueLabel: controls.exposure === 0 ? void 0 : `${controls.exposure > 0 ? "+" : ""}${controls.exposure}`,
2012
+ onPress: () => controls.setExposure(controls.exposure === 0 ? 1 : 0)
2013
+ }
2014
+ ] : []
2015
+ ];
2016
+ const railActions = [...coreRail, ...slots.railActions ?? []];
2017
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "absolute inset-0 select-none overflow-hidden bg-black", children: [
2018
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "absolute inset-0", children: preview }),
2019
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(GridOverlay, { visible: gridOn && !blocked }),
2020
+ aspect !== "full" && !blocked && !engine.recording && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2021
+ "div",
2022
+ {
2023
+ "aria-hidden": true,
2024
+ className: "pointer-events-none absolute inset-0 z-10 flex items-center justify-center",
2025
+ children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2026
+ "div",
2027
+ {
2028
+ className: "shadow-[0_0_0_9999px_rgba(0,0,0,0.45)]",
2029
+ style: {
2030
+ aspectRatio: aspect === "1:1" ? "1 / 1" : aspect === "4:3" ? "3 / 4" : "9 / 16",
2031
+ width: aspect === "16:9" ? "100%" : void 0,
2032
+ height: aspect === "16:9" ? void 0 : "70%",
2033
+ maxWidth: "100%",
2034
+ maxHeight: "100%"
2035
+ }
2036
+ }
2037
+ )
2038
+ }
2039
+ ),
2040
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(CountdownOverlay, { seconds: countdown }),
2041
+ !controlsHidden && /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
2042
+ "div",
2043
+ {
2044
+ className: "pointer-events-none absolute inset-x-0 top-0 z-20 px-2",
2045
+ style: safeTop,
2046
+ children: [
2047
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "flex min-h-[44px] items-center gap-2 py-1.5", children: [
2048
+ onClose ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2049
+ "button",
2050
+ {
2051
+ type: "button",
2052
+ onClick: onClose,
2053
+ "aria-label": "Close camera",
2054
+ className: "pointer-events-auto flex h-10 w-10 shrink-0 touch-manipulation items-center justify-center rounded-full text-white drop-shadow-[0_1px_3px_rgba(0,0,0,0.6)] transition-colors hover:bg-white/10",
2055
+ children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react5.X, { className: "h-6 w-6" })
2056
+ }
2057
+ ) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "h-10 w-10 shrink-0" }),
2058
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "pointer-events-auto flex min-w-0 flex-1 justify-center", children: slots.topEntry }),
2059
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "h-10 w-10 shrink-0" })
2060
+ ] }),
2061
+ slots.topCenter ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "pointer-events-none pb-1 text-center", children: slots.topCenter }) : null
2062
+ ]
2063
+ }
2064
+ ),
2065
+ slots.statusChips ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2066
+ "div",
2067
+ {
2068
+ className: "pointer-events-none absolute inset-x-0 z-20 flex flex-col items-center gap-1.5 px-3",
2069
+ style: {
2070
+ top: `calc(env(safe-area-inset-top, 0px) + ${controlsHidden ? 12 : 64}px)`
2071
+ },
2072
+ children: slots.statusChips
2073
+ }
2074
+ ) : null,
2075
+ !controlsHidden && !blocked && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2076
+ "div",
2077
+ {
2078
+ className: "pointer-events-none absolute right-1 z-20 flex justify-end",
2079
+ style: { top: `calc(env(safe-area-inset-top, 0px) + 64px)` },
2080
+ children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(CaptureRail, { actions: railActions })
2081
+ }
2082
+ ),
2083
+ !controlsHidden && /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "absolute inset-x-0 bottom-0 z-20", style: safeBottom, children: [
2084
+ media && media.items.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "px-2 pb-1.5", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2085
+ CaptureFilmstrip,
2086
+ {
2087
+ items: media.items,
2088
+ onOpen: (item) => setViewerKey(item.key)
2089
+ }
2090
+ ) }) : null,
2091
+ slots.aboveShutter ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "px-2 pb-1.5", children: slots.aboveShutter }) : null,
2092
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "flex items-center justify-between px-3 pb-2", children: [
2093
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "flex w-[86px] justify-start", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2094
+ "button",
2095
+ {
2096
+ type: "button",
2097
+ onClick: cloud.onOpenLibrary,
2098
+ "aria-label": "Your media \u2014 library and upload",
2099
+ className: "relative h-12 w-12 touch-manipulation overflow-hidden rounded-xl bg-black/40 ring-1 ring-white/30 backdrop-blur-md transition-transform active:scale-95",
2100
+ children: cloud.recentsThumb ?? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "flex h-full w-full items-center justify-center", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react5.Images, { className: "h-5 w-5 text-white/80" }) })
2101
+ }
2102
+ ) }),
2103
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2104
+ HoldShutter,
2105
+ {
2106
+ recording: engine.recording,
2107
+ elapsedSeconds: engine.recordElapsedSeconds,
2108
+ ...maxRecordSeconds !== void 0 ? { maxSeconds: maxRecordSeconds } : {},
2109
+ disabled: shutterDisabled || blocked,
2110
+ ...onRecordIntent ? { onPressStart: onRecordIntent } : {},
2111
+ onPhoto,
2112
+ onStartRecording: engine.onStartRecording,
2113
+ onStopRecording: engine.onStopRecording
2114
+ }
2115
+ ),
2116
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "flex w-[86px] justify-end", children: slots.shutterTrailing })
2117
+ ] }),
2118
+ !engine.recording && !blocked ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { className: "pb-1.5 text-center text-[11px] text-white/60 drop-shadow-[0_1px_2px_rgba(0,0,0,0.8)]", children: "Tap for a photo \xB7 hold to record" }) : null
2119
+ ] }),
2120
+ blocked && blockedSheet && !blockedDismissed && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2121
+ CaptureSheet,
2122
+ {
2123
+ open: true,
2124
+ onClose: () => setBlockedDismissed(true),
2125
+ body: blockedSheet.body,
2126
+ title: "Camera unavailable",
2127
+ actions: blockedSheet.actions
2128
+ }
2129
+ ),
2130
+ media && viewerKey !== null && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2131
+ MediaViewer,
2132
+ {
2133
+ items: media.items,
2134
+ initialKey: viewerKey,
2135
+ keysDisabled: editTarget !== null,
2136
+ onClose: () => setViewerKey(null),
2137
+ onDelete: media.onDelete ? (item) => {
2138
+ media.onDelete?.(item.key);
2139
+ invalidateMedia(item.key);
2140
+ } : void 0,
2141
+ onEdit: media.onReplacePhoto ? (item) => {
2142
+ const url = getMediaUrl(item);
2143
+ if (url) setEditTarget({ key: item.key, url });
2144
+ } : void 0
2145
+ }
2146
+ ),
2147
+ media?.onReplacePhoto && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2148
+ ImageEditSheet,
2149
+ {
2150
+ open: editTarget !== null,
2151
+ src: editTarget?.url ?? null,
2152
+ onClose: () => setEditTarget(null),
2153
+ onSave: (blob) => {
2154
+ const target = editTarget;
2155
+ setEditTarget(null);
2156
+ setViewerKey(null);
2157
+ if (target) {
2158
+ media.onReplacePhoto?.(target.key, blob);
2159
+ invalidateMedia(target.key);
2160
+ }
2161
+ }
2162
+ }
2163
+ ),
2164
+ slots.overlays
2165
+ ] });
2166
+ }
2167
+
2168
+ // src/components/CaptureExpandingField.tsx
2169
+ var import_react9 = require("react");
2170
+ var import_jsx_runtime15 = require("react/jsx-runtime");
2171
+ function CaptureExpandingField({
2172
+ label,
2173
+ placeholder,
2174
+ icon,
2175
+ onCommit,
2176
+ defaultExpanded = false,
2177
+ disabled = false,
2178
+ currentValue = null,
2179
+ className
2180
+ }) {
2181
+ const [expanded, setExpanded] = (0, import_react9.useState)(defaultExpanded);
2182
+ const [draft, setDraft] = (0, import_react9.useState)("");
2183
+ const inputRef = (0, import_react9.useRef)(null);
2184
+ const draftRef = (0, import_react9.useRef)(draft);
2185
+ draftRef.current = draft;
2186
+ const onCommitRef = (0, import_react9.useRef)(onCommit);
2187
+ onCommitRef.current = onCommit;
2188
+ (0, import_react9.useEffect)(() => {
2189
+ return () => {
2190
+ const trimmed = draftRef.current.trim();
2191
+ if (trimmed) onCommitRef.current(trimmed);
2192
+ };
2193
+ }, []);
2194
+ const commit = (0, import_react9.useCallback)(() => {
2195
+ const trimmed = draftRef.current.trim();
2196
+ if (trimmed) onCommitRef.current(trimmed);
2197
+ setDraft("");
2198
+ }, []);
2199
+ const open = (0, import_react9.useCallback)(() => {
2200
+ setExpanded(true);
2201
+ requestAnimationFrame(() => inputRef.current?.focus());
2202
+ }, []);
2203
+ const close = (0, import_react9.useCallback)(() => {
2204
+ commit();
2205
+ setExpanded(false);
2206
+ }, [commit]);
2207
+ if (!expanded) {
2208
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
2209
+ "button",
2210
+ {
2211
+ type: "button",
2212
+ onClick: open,
2213
+ disabled,
2214
+ "aria-label": label,
2215
+ "aria-expanded": false,
2216
+ className: cn(
2217
+ "flex h-9 max-w-[62vw] items-center gap-1.5 rounded-full px-3.5",
2218
+ "bg-black/45 text-[13px] font-semibold text-white backdrop-blur-md",
2219
+ "transition-colors hover:bg-black/60 active:bg-black/70",
2220
+ disabled && "opacity-40",
2221
+ className
2222
+ ),
2223
+ children: [
2224
+ icon ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "shrink-0", children: icon }) : null,
2225
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "truncate", children: currentValue?.trim() || label })
2226
+ ]
2227
+ }
2228
+ );
2229
+ }
2230
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
2231
+ "div",
2232
+ {
2233
+ className: cn(
2234
+ "flex h-9 w-[min(78vw,320px)] items-center gap-1.5 rounded-full px-3",
2235
+ "bg-black/60 backdrop-blur-md",
2236
+ className
2237
+ ),
2238
+ children: [
2239
+ icon ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "shrink-0 text-white/70", children: icon }) : null,
2240
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2241
+ "input",
2242
+ {
2243
+ ref: inputRef,
2244
+ value: draft,
2245
+ onChange: (e) => setDraft(e.target.value),
2246
+ onBlur: close,
2247
+ onKeyDown: (e) => {
2248
+ if (e.key === "Enter") {
2249
+ commit();
2250
+ e.target.blur();
2251
+ }
2252
+ if (e.key === "Escape") {
2253
+ setDraft("");
2254
+ setExpanded(false);
2255
+ }
2256
+ },
2257
+ placeholder: placeholder ?? label,
2258
+ "aria-label": label,
2259
+ enterKeyHint: "done",
2260
+ autoCapitalize: "characters",
2261
+ autoCorrect: "off",
2262
+ spellCheck: false,
2263
+ className: cn(
2264
+ "min-w-0 flex-1 bg-transparent text-base text-white",
2265
+ "placeholder:text-white/40 focus:outline-none"
2266
+ )
2267
+ }
2268
+ ),
2269
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2270
+ "button",
2271
+ {
2272
+ type: "button",
2273
+ onMouseDown: (e) => e.preventDefault(),
2274
+ onClick: () => {
2275
+ setDraft("");
2276
+ setExpanded(false);
2277
+ },
2278
+ "aria-label": `Close ${label}`,
2279
+ className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-white/70 hover:bg-white/10 hover:text-white",
2280
+ children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2281
+ "svg",
2282
+ {
2283
+ viewBox: "0 0 24 24",
2284
+ fill: "none",
2285
+ stroke: "currentColor",
2286
+ strokeWidth: "2.5",
2287
+ strokeLinecap: "round",
2288
+ className: "h-3.5 w-3.5",
2289
+ "aria-hidden": "true",
2290
+ children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("path", { d: "M18 6 6 18M6 6l12 12" })
2291
+ }
2292
+ )
2293
+ }
2294
+ )
2295
+ ]
2296
+ }
2297
+ );
2298
+ }
2299
+
2300
+ // src/components/CameraFeed.tsx
2301
+ var import_react10 = require("react");
2302
+ var import_jsx_runtime16 = require("react/jsx-runtime");
1644
2303
  function CameraFeed({
1645
2304
  engine,
1646
2305
  mirror = false
1647
2306
  }) {
1648
2307
  const { stream, videoRef } = engine;
1649
- (0, import_react6.useEffect)(() => {
2308
+ (0, import_react10.useEffect)(() => {
1650
2309
  const video = videoRef.current;
1651
2310
  if (!video) return;
1652
2311
  if (video.srcObject !== stream) video.srcObject = stream;
@@ -1654,7 +2313,7 @@ function CameraFeed({
1654
2313
  video.srcObject = null;
1655
2314
  };
1656
2315
  }, [stream, videoRef]);
1657
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2316
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1658
2317
  "video",
1659
2318
  {
1660
2319
  ref: videoRef,
@@ -1672,7 +2331,7 @@ function CameraFeed({
1672
2331
  }
1673
2332
 
1674
2333
  // src/engine/useDefaultEngine.ts
1675
- var import_react7 = require("react");
2334
+ var import_react11 = require("react");
1676
2335
  var RECORDING_MIME_LADDER = [
1677
2336
  'video/mp4;codecs="avc1.42E01E,mp4a.40.2"',
1678
2337
  "video/mp4",
@@ -1702,18 +2361,18 @@ function useDefaultCaptureEngine(options) {
1702
2361
  facingMode: initialFacing = "environment",
1703
2362
  photoQuality = 0.92
1704
2363
  } = options;
1705
- const [stream, setStream] = (0, import_react7.useState)(null);
1706
- const [blocked, setBlocked] = (0, import_react7.useState)(null);
1707
- const [facing, setFacing] = (0, import_react7.useState)(initialFacing);
1708
- const [multipleCameras, setMultipleCameras] = (0, import_react7.useState)(false);
1709
- const [recording, setRecording] = (0, import_react7.useState)(false);
1710
- const [recordElapsedSeconds, setRecordElapsedSeconds] = (0, import_react7.useState)(0);
1711
- const videoRef = (0, import_react7.useRef)(null);
1712
- const streamRef = (0, import_react7.useRef)(null);
1713
- const recorderRef = (0, import_react7.useRef)(null);
1714
- const callbacksRef = (0, import_react7.useRef)({ onPhoto, onVideo, onFiles });
2364
+ const [stream, setStream] = (0, import_react11.useState)(null);
2365
+ const [blocked, setBlocked] = (0, import_react11.useState)(null);
2366
+ const [facing, setFacing] = (0, import_react11.useState)(initialFacing);
2367
+ const [multipleCameras, setMultipleCameras] = (0, import_react11.useState)(false);
2368
+ const [recording, setRecording] = (0, import_react11.useState)(false);
2369
+ const [recordElapsedSeconds, setRecordElapsedSeconds] = (0, import_react11.useState)(0);
2370
+ const videoRef = (0, import_react11.useRef)(null);
2371
+ const streamRef = (0, import_react11.useRef)(null);
2372
+ const recorderRef = (0, import_react11.useRef)(null);
2373
+ const callbacksRef = (0, import_react11.useRef)({ onPhoto, onVideo, onFiles });
1715
2374
  callbacksRef.current = { onPhoto, onVideo, onFiles };
1716
- (0, import_react7.useEffect)(() => {
2375
+ (0, import_react11.useEffect)(() => {
1717
2376
  let cancelled = false;
1718
2377
  let acquired = null;
1719
2378
  if (typeof navigator === "undefined" || !navigator.mediaDevices) {
@@ -1756,7 +2415,7 @@ function useDefaultCaptureEngine(options) {
1756
2415
  setStream(null);
1757
2416
  };
1758
2417
  }, [facing]);
1759
- (0, import_react7.useEffect)(() => {
2418
+ (0, import_react11.useEffect)(() => {
1760
2419
  return () => {
1761
2420
  const r = recorderRef.current;
1762
2421
  if (r) {
@@ -1766,7 +2425,7 @@ function useDefaultCaptureEngine(options) {
1766
2425
  }
1767
2426
  };
1768
2427
  }, []);
1769
- const onCapturePhoto = (0, import_react7.useCallback)(
2428
+ const onCapturePhoto = (0, import_react11.useCallback)(
1770
2429
  (opts) => {
1771
2430
  const video = videoRef.current;
1772
2431
  if (!video || video.videoWidth === 0) return;
@@ -1806,7 +2465,7 @@ function useDefaultCaptureEngine(options) {
1806
2465
  },
1807
2466
  [photoQuality]
1808
2467
  );
1809
- const onStartRecording = (0, import_react7.useCallback)(() => {
2468
+ const onStartRecording = (0, import_react11.useCallback)(() => {
1810
2469
  const base = streamRef.current;
1811
2470
  if (!base || recorderRef.current) return;
1812
2471
  void (async () => {
@@ -1873,11 +2532,11 @@ function useDefaultCaptureEngine(options) {
1873
2532
  setRecording(true);
1874
2533
  })();
1875
2534
  }, [withAudio]);
1876
- const onStopRecording = (0, import_react7.useCallback)(() => {
2535
+ const onStopRecording = (0, import_react11.useCallback)(() => {
1877
2536
  const r = recorderRef.current;
1878
2537
  if (r && r.recorder.state !== "inactive") r.recorder.stop();
1879
2538
  }, []);
1880
- const onUpload = (0, import_react7.useCallback)(() => {
2539
+ const onUpload = (0, import_react11.useCallback)(() => {
1881
2540
  const input = document.createElement("input");
1882
2541
  input.type = "file";
1883
2542
  input.accept = "image/*,video/*";
@@ -1888,10 +2547,10 @@ function useDefaultCaptureEngine(options) {
1888
2547
  };
1889
2548
  input.click();
1890
2549
  }, []);
1891
- const onFlipCamera = (0, import_react7.useCallback)(() => {
2550
+ const onFlipCamera = (0, import_react11.useCallback)(() => {
1892
2551
  setFacing((f) => f === "environment" ? "user" : "environment");
1893
2552
  }, []);
1894
- return (0, import_react7.useMemo)(
2553
+ return (0, import_react11.useMemo)(
1895
2554
  () => ({
1896
2555
  stream,
1897
2556
  videoRef,