{"version":3,"sources":["../src/dnd.ts"],"sourcesContent":["// Cross-app drag-out (FILE_EXPLORER_SPEC §7; UI_AS_APPS_SPEC §2 host-mediated).\n//\n// Native HTML5 drag-and-drop does NOT cross the sandboxed cross-origin iframe\n// boundary, and pointer events inside one app's iframe never reach the host or a\n// sibling. So a drag that STARTS in one app (the file explorer) and ENDS over\n// another (the previewed app) can only be mediated by the host (the TCB). This\n// module is the SDK surface for that one net-new platform primitive:\n//\n//  - SOURCE side (the file explorer, needs the first-party `dnd:source` cap):\n//    `startItemDrag(item)` asks the host to begin a host-mediated drag carrying a\n//    file/dir reference (+ optional inlined bytes for a small file). The host draws\n//    the trusted drag ghost, tracks the pointer across regions, and on drop over the\n//    preview delivers the item to that app. `cancelItemDrag()` aborts.\n//  - RECEIVER side (the previewed app, NO new grant — it opts in by subscribing):\n//    `onItemDrop(cb)` / `useDroppedItem()` deliver the dropped item with host-attached,\n//    unspoofable provenance (`from` = source region id) and the drop position.\n//\n// The payload is UNTRUSTED app data (CLAUDE.md §5): the host attaches `from`; the app\n// validates everything else. v1 inlines bytes only for small files (the source can\n// only relay data it can already read — no new read authority is minted).\nimport { useEffect, useState } from 'react';\nimport { throwOnRefusal } from './protocolRefusal';\nimport { protocolRequest, sendMessage, addListener } from './sandboxUtils';\nimport { DND_CANCEL, DROPPED_ITEM, PROTOCOL_DND } from './generated/protocol';\nimport { SCHEMES } from './protocolSchemes';\n\n/** A file/dir being dragged out of an app. `bytes` is present only for a small file\n *  the source chose to inline (transferred zero-copy); a dir or an over-cap file\n *  carries the reference only (`kind`/`name`/`mountId`/`relPath`). */\nexport interface DraggableItem {\n  kind: 'file' | 'dir';\n  /** Basename — display only. */\n  name: string;\n  /** Which mounted filesystem the item lives in. */\n  mountId: string;\n  /** Path within that mount (leading slash, no `..`). */\n  relPath: string;\n  /** Optional inlined content for a small file. */\n  bytes?: Uint8Array;\n}\n\n/** An item dropped onto THIS app by a host-mediated cross-app drag. */\nexport interface DroppedItem {\n  /** The dragged item (`bytes` present iff the source inlined them). */\n  item: DraggableItem;\n  /** Host-attached source region id — unspoofable (T19), like an `ipc` `from`. */\n  from: string;\n  /** Drop point in this app's viewport. */\n  position: { x: number; y: number };\n}\n\n/** An error from {@link startItemDrag}, carrying a machine-readable `.code`.\n *\n *  **What you actually catch.** The SDK throws a plain `Error` with `.code` assigned — a\n *  `CodedRefusalError` from `protocolRefusal.ts` — never a distinct class. This interface\n *  documents the `.code` VALUES the host sends. Nothing enforces the union at runtime —\n *  `.code` is whatever string arrived — so treat an unlisted code as possible and\n *  `instanceof Error` as the only reliable test.\n */\nexport interface ItemDragError extends Error {\n  code:\n    | 'forbidden' // the frame lacks the first-party `dnd:source` capability\n    | 'invalid-params' // the item was malformed (empty path / `..` / URI / bad kind)\n    | 'too-large' // inlined `bytes` exceed the host's size limit\n    | 'rate-limited' // too many drags started too fast (capacity-class, fail-open)\n    | 'unknown';\n}\n\n/**\n * Begin a host-mediated drag of `item` out of this app. Resolves once the host has\n * taken over the drag (drawn the ghost, installed the pointer-capture layer); rejects\n * with an {@link ItemDragError} if this app may not initiate drags (`forbidden`) or the\n * item is invalid. Only a first-party chrome app holding `dnd:source` may call this — a\n * previewed/third-party app is refused at the gate (it must not synthesize drags into\n * sibling apps).\n */\nexport const startItemDrag = async (item: DraggableItem): Promise<void> => {\n  const res = (await protocolRequest(SCHEMES[PROTOCOL_DND], 'startDrag', [item])) as\n    | { ok: true }\n    | { ok: false; code?: string; message?: string }\n    | undefined;\n  throwOnRefusal(res, 'dnd startDrag failed');\n};\n\n/** Abort an in-progress host-mediated drag this app started (e.g. the user pressed\n *  Escape, or the gesture was cancelled). Best-effort and fire-and-forget. */\nexport const cancelItemDrag = (): void => {\n  sendMessage(DND_CANCEL, {});\n};\n\n/** Subscribe to items dropped onto this app by a host-mediated cross-app drag.\n *  Returns an unsubscribe fn. Subscribing is the opt-in: an app that never subscribes\n *  receives nothing (the host shows a \"not accepted\" cue and the drop is a no-op). */\nexport const onItemDrop = (listener: (d: DroppedItem) => void): (() => void) =>\n  addListener(DROPPED_ITEM, (m: { item: DraggableItem; from: string; position: { x: number; y: number } }) =>\n    listener({ item: m.item, from: m.from, position: m.position }),\n  );\n\n/** React hook: the most recently dropped item (or `null`). */\nexport const useDroppedItem = (): DroppedItem | null => {\n  const [dropped, setDropped] = useState<DroppedItem | null>(null);\n  useEffect(() => onItemDrop(setDropped), []);\n  return dropped;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAoBA,mBAAoC;AACpC,6BAA+B;AAC/B,0BAA0D;AAC1D,sBAAuD;AACvD,6BAAwB;AAoDjB,MAAM,gBAAgB,OAAO,SAAuC;AACzE,QAAM,MAAO,UAAM,qCAAgB,+BAAQ,4BAAY,GAAG,aAAa,CAAC,IAAI,CAAC;AAI7E,6CAAe,KAAK,sBAAsB;AAC5C;AAIO,MAAM,iBAAiB,MAAY;AACxC,uCAAY,4BAAY,CAAC,CAAC;AAC5B;AAKO,MAAM,aAAa,CAAC,iBACzB;AAAA,EAAY;AAAA,EAAc,CAAC,MACzB,SAAS,EAAE,MAAM,EAAE,MAAM,MAAM,EAAE,MAAM,UAAU,EAAE,SAAS,CAAC;AAC/D;AAGK,MAAM,iBAAiB,MAA0B;AACtD,QAAM,CAAC,SAAS,UAAU,QAAI,uBAA6B,IAAI;AAC/D,8BAAU,MAAM,WAAW,UAAU,GAAG,CAAC,CAAC;AAC1C,SAAO;AACT;","names":[]}