@ai-matrx/capture 0.3.1 → 0.4.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
@@ -446,14 +446,18 @@ declare function CaptureRail({ actions, defaultExpanded, className, }: CaptureRa
446
446
  * pointerdown → arm a hold timer (HOLD_MS)
447
447
  * timer fires → start recording, ring begins filling
448
448
  * pointerup < HOLD_MS → cancel the timer, take a photo
449
- * pointerup >= HOLD_MS → stop recording
449
+ * pointerup >= HOLD_MS → stop recording (recording follows the finger)
450
+ * slide up ≥ LOCK_DRAG_PX while holding → LOCK: release keeps recording,
451
+ * the next tap stops
450
452
  * pointercancel / leave → same as pointerup (a drag off the button must not
451
453
  * leave the camera recording with nothing to stop it)
452
454
  *
453
- * A LOCK is deliberate: after ~1.2s of recording the button latches, so a user
454
- * whose finger slips does not lose the take. Once latched, the next tap stops.
455
- * Without it, the single-button model punishes exactly the long recordings it
456
- * exists to make easy.
455
+ * A LOCK is a GESTURE, never a timer (Arman, 2026-08-30: recording follows
456
+ * the finger releasing stops, full stop unless the user deliberately
457
+ * locks): while holding a recording, slide UP to the lock target that
458
+ * appears above the button. Locked, release keeps recording and the next
459
+ * tap stops. An automatic time-based latch shipped first and was wrong —
460
+ * it silently converted "I let go" into "still recording".
457
461
  *
458
462
  * Presentational only — the host owns the recorder.
459
463
  */
package/dist/react.d.ts CHANGED
@@ -446,14 +446,18 @@ declare function CaptureRail({ actions, defaultExpanded, className, }: CaptureRa
446
446
  * pointerdown → arm a hold timer (HOLD_MS)
447
447
  * timer fires → start recording, ring begins filling
448
448
  * pointerup < HOLD_MS → cancel the timer, take a photo
449
- * pointerup >= HOLD_MS → stop recording
449
+ * pointerup >= HOLD_MS → stop recording (recording follows the finger)
450
+ * slide up ≥ LOCK_DRAG_PX while holding → LOCK: release keeps recording,
451
+ * the next tap stops
450
452
  * pointercancel / leave → same as pointerup (a drag off the button must not
451
453
  * leave the camera recording with nothing to stop it)
452
454
  *
453
- * A LOCK is deliberate: after ~1.2s of recording the button latches, so a user
454
- * whose finger slips does not lose the take. Once latched, the next tap stops.
455
- * Without it, the single-button model punishes exactly the long recordings it
456
- * exists to make easy.
455
+ * A LOCK is a GESTURE, never a timer (Arman, 2026-08-30: recording follows
456
+ * the finger releasing stops, full stop unless the user deliberately
457
+ * locks): while holding a recording, slide UP to the lock target that
458
+ * appears above the button. Locked, release keeps recording and the next
459
+ * tap stops. An automatic time-based latch shipped first and was wrong —
460
+ * it silently converted "I let go" into "still recording".
457
461
  *
458
462
  * Presentational only — the host owns the recorder.
459
463
  */
package/dist/react.js CHANGED
@@ -601,7 +601,7 @@ import {
601
601
  useRef,
602
602
  useState as useState3
603
603
  } from "react";
604
- import { ChevronLeft, ChevronRight, Pencil, Trash2, X as X2 } from "lucide-react";
604
+ import { ChevronLeft, ChevronRight, Pencil, Play, Trash2, X as X2 } from "lucide-react";
605
605
  import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
606
606
  var SWIPE_TRIGGER_PX = 40;
607
607
  var SWIPE_TRIGGER_VELOCITY = 0.25;
@@ -624,6 +624,7 @@ function MediaViewer({
624
624
  const clamped = Math.min(index, count - 1);
625
625
  const current = count > 0 ? items[clamped] : void 0;
626
626
  const [confirmingDelete, setConfirmingDelete] = useState3(false);
627
+ const activeVideoRef = useRef(null);
627
628
  const dragRef = useRef(null);
628
629
  const [drag, setDrag] = useState3(null);
629
630
  const [settling, setSettling] = useState3(false);
@@ -664,7 +665,7 @@ function MediaViewer({
664
665
  return () => window.removeEventListener("keydown", onKey);
665
666
  }, [go, onClose, keysDisabled]);
666
667
  const onPointerDown = useCallback2((e) => {
667
- if (e.target.closest("video, audio")) return;
668
+ if (e.target.closest("audio")) return;
668
669
  dragRef.current = {
669
670
  x0: e.clientX,
670
671
  y0: e.clientY,
@@ -711,6 +712,14 @@ function MediaViewer({
711
712
  return;
712
713
  }
713
714
  }
715
+ if (d.axis === null) {
716
+ const video = activeVideoRef.current;
717
+ if (video) {
718
+ if (video.paused) void video.play().catch(() => {
719
+ });
720
+ else video.pause();
721
+ }
722
+ }
714
723
  setDrag(null);
715
724
  setSettling(true);
716
725
  },
@@ -787,7 +796,15 @@ function MediaViewer({
787
796
  onPointerMove,
788
797
  onPointerUp: endDrag,
789
798
  onPointerCancel: endDrag,
790
- children: /* @__PURE__ */ jsx9(ViewerSlide, { item: current, active: true }, current.key)
799
+ children: /* @__PURE__ */ jsx9(
800
+ ViewerSlide,
801
+ {
802
+ item: current,
803
+ active: true,
804
+ videoRef: activeVideoRef
805
+ },
806
+ current.key
807
+ )
791
808
  }
792
809
  ),
793
810
  /* @__PURE__ */ jsx9(
@@ -880,7 +897,8 @@ function MediaViewer({
880
897
  }
881
898
  function ViewerSlide({
882
899
  item,
883
- active
900
+ active,
901
+ videoRef
884
902
  }) {
885
903
  const url = useMediaUrl(item);
886
904
  if (!url) {
@@ -909,20 +927,7 @@ function ViewerSlide({
909
927
  ] });
910
928
  }
911
929
  if (item.kind === "video") {
912
- return /* @__PURE__ */ jsx9(
913
- "video",
914
- {
915
- src: url,
916
- controls: active,
917
- playsInline: true,
918
- preload: active ? "auto" : "metadata",
919
- muted: !active,
920
- className: cn(
921
- "absolute inset-0 h-full w-full object-contain",
922
- active ? "pointer-events-auto" : "pointer-events-none"
923
- )
924
- }
925
- );
930
+ return /* @__PURE__ */ jsx9(VideoSlide, { url, active, videoRef });
926
931
  }
927
932
  return /* @__PURE__ */ jsx9(
928
933
  "img",
@@ -935,6 +940,51 @@ function ViewerSlide({
935
940
  }
936
941
  );
937
942
  }
943
+ function VideoSlide({
944
+ url,
945
+ active,
946
+ videoRef
947
+ }) {
948
+ const [paused, setPaused] = useState3(true);
949
+ const [progress, setProgress] = useState3(0);
950
+ const localRef = useRef(null);
951
+ useEffect3(() => {
952
+ if (!active || !videoRef) return;
953
+ videoRef.current = localRef.current;
954
+ return () => {
955
+ if (videoRef.current === localRef.current) videoRef.current = null;
956
+ };
957
+ }, [active, videoRef]);
958
+ return /* @__PURE__ */ jsxs7("div", { className: "pointer-events-none absolute inset-0", children: [
959
+ /* @__PURE__ */ jsx9(
960
+ "video",
961
+ {
962
+ ref: localRef,
963
+ src: url,
964
+ playsInline: true,
965
+ preload: active ? "auto" : "metadata",
966
+ muted: !active,
967
+ loop: false,
968
+ onPlay: () => setPaused(false),
969
+ onPause: () => setPaused(true),
970
+ onEnded: () => setPaused(true),
971
+ onTimeUpdate: (e) => {
972
+ const v = e.currentTarget;
973
+ setProgress(v.duration > 0 ? v.currentTime / v.duration : 0);
974
+ },
975
+ className: "absolute inset-0 h-full w-full object-contain"
976
+ }
977
+ ),
978
+ active && paused && /* @__PURE__ */ jsx9("span", { className: "absolute inset-0 flex items-center justify-center", children: /* @__PURE__ */ jsx9("span", { className: "flex h-16 w-16 items-center justify-center rounded-full bg-black/45", children: /* @__PURE__ */ jsx9(Play, { className: "ml-1 h-8 w-8 fill-white text-white" }) }) }),
979
+ active && !paused && /* @__PURE__ */ jsx9("span", { className: "absolute inset-x-4 bottom-2 h-0.5 overflow-hidden rounded-full bg-white/25", children: /* @__PURE__ */ jsx9(
980
+ "span",
981
+ {
982
+ className: "block h-full rounded-full bg-white",
983
+ style: { width: `${Math.round(progress * 100)}%` }
984
+ }
985
+ ) })
986
+ ] });
987
+ }
938
988
 
939
989
  // src/components/ImageEditSheet.tsx
940
990
  import {
@@ -1726,9 +1776,10 @@ function Chevron({ className }) {
1726
1776
 
1727
1777
  // src/components/HoldShutter.tsx
1728
1778
  import { useCallback as useCallback5, useEffect as useEffect6, useRef as useRef4, useState as useState7 } from "react";
1779
+ import { Lock } from "lucide-react";
1729
1780
  import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
1730
1781
  var HOLD_MS = 320;
1731
- var LOCK_AFTER_MS = 1200;
1782
+ var LOCK_DRAG_PX = 64;
1732
1783
  function HoldShutter({
1733
1784
  recording,
1734
1785
  elapsedSeconds,
@@ -1741,6 +1792,7 @@ function HoldShutter({
1741
1792
  }) {
1742
1793
  const holdTimer = useRef4(null);
1743
1794
  const startedRecording = useRef4(false);
1795
+ const pressOriginY = useRef4(0);
1744
1796
  const [pressed, setPressed] = useState7(false);
1745
1797
  const [locked, setLocked] = useState7(false);
1746
1798
  const clearHold = useCallback5(() => {
@@ -1750,17 +1802,14 @@ function HoldShutter({
1750
1802
  }
1751
1803
  }, []);
1752
1804
  useEffect6(() => {
1753
- if (!recording) {
1754
- setLocked(false);
1755
- return;
1756
- }
1757
- if (elapsedSeconds * 1e3 >= LOCK_AFTER_MS) setLocked(true);
1758
- }, [recording, elapsedSeconds]);
1805
+ if (!recording) setLocked(false);
1806
+ }, [recording]);
1759
1807
  useEffect6(() => clearHold, [clearHold]);
1760
1808
  const onPointerDown = useCallback5(
1761
1809
  (event) => {
1762
1810
  if (disabled) return;
1763
1811
  event.currentTarget.setPointerCapture?.(event.pointerId);
1812
+ pressOriginY.current = event.clientY;
1764
1813
  setPressed(true);
1765
1814
  onPressStart?.();
1766
1815
  if (recording && locked) return;
@@ -1773,6 +1822,16 @@ function HoldShutter({
1773
1822
  },
1774
1823
  [disabled, locked, onPressStart, onStartRecording, recording]
1775
1824
  );
1825
+ const onPointerMove = useCallback5(
1826
+ (event) => {
1827
+ if (!pressed || locked || !startedRecording.current) return;
1828
+ if (pressOriginY.current - event.clientY >= LOCK_DRAG_PX) {
1829
+ setLocked(true);
1830
+ navigator.vibrate?.(30);
1831
+ }
1832
+ },
1833
+ [pressed, locked]
1834
+ );
1776
1835
  const endPress = useCallback5(() => {
1777
1836
  if (!pressed) return;
1778
1837
  setPressed(false);
@@ -1828,12 +1887,34 @@ function HoldShutter({
1828
1887
  ]
1829
1888
  }
1830
1889
  ) : null,
1890
+ recording && pressed && !locked && /* @__PURE__ */ jsxs11(
1891
+ "span",
1892
+ {
1893
+ className: "pointer-events-none absolute left-1/2 flex -translate-x-1/2 flex-col items-center gap-0.5",
1894
+ style: { top: -LOCK_DRAG_PX - 22 },
1895
+ "aria-hidden": true,
1896
+ children: [
1897
+ /* @__PURE__ */ jsx13("span", { className: "flex h-9 w-9 items-center justify-center rounded-full bg-black/60 ring-1 ring-white/30", children: /* @__PURE__ */ jsx13(Lock, { className: "h-4 w-4 text-white/90" }) }),
1898
+ /* @__PURE__ */ jsx13("span", { className: "text-[10px] font-medium text-white/80 drop-shadow", children: "Slide up to lock" })
1899
+ ]
1900
+ }
1901
+ ),
1902
+ recording && locked && /* @__PURE__ */ jsx13(
1903
+ "span",
1904
+ {
1905
+ className: "pointer-events-none absolute left-1/2 flex h-9 w-9 -translate-x-1/2 items-center justify-center rounded-full bg-[#FF3B30]",
1906
+ style: { top: -LOCK_DRAG_PX - 22 },
1907
+ "aria-hidden": true,
1908
+ children: /* @__PURE__ */ jsx13(Lock, { className: "h-4 w-4 text-white", fill: "currentColor" })
1909
+ }
1910
+ ),
1831
1911
  /* @__PURE__ */ jsx13(
1832
1912
  "button",
1833
1913
  {
1834
1914
  type: "button",
1835
1915
  disabled,
1836
1916
  onPointerDown,
1917
+ onPointerMove,
1837
1918
  onPointerUp: endPress,
1838
1919
  onPointerCancel: endPress,
1839
1920
  onLostPointerCapture: endPress,