> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cskn.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Camera

> Take photos with the system camera, or open the shell's full-screen camera for photos and video.

There are two ways to get a picture from the camera.

| | `useCamera` / `capturePhoto` | `useCameraShot` / `openCamera` |
| - | - | - |
| UI | The operating system's camera app | The shell's own full-screen camera |
| Output | A photo | A photo or a video clip |
| In a browser | Falls back to a file picker | `not_available` |

## Photo with the system camera

```tsx theme={null}
import { useCamera } from '@cskn/sdk';

function TakePhoto() {
  const { capture, loading } = useCamera();

  const onClick = async () => {
    const result = await capture({ quality: 0.8, allowsEditing: true });
    if (result.cancelled || result.error) return;
    upload(result.asset!.uri); // data:image/jpeg;base64,…
  };

  return <button onClick={onClick}>{loading ? 'Opening…' : 'Take photo'}</button>;
}
```

<ParamField path="quality" type="number" default="0.7">
  JPEG quality from `0` to `1`.
</ParamField>

<ParamField path="allowsEditing" type="boolean" default="false">
  Show the crop step after capture.
</ParamField>

The result is `{ asset?, cancelled, error? }`. `asset` has `uri` (a base64 data URL), `width`, `height`, `fileName` and `mimeType`. `error` is `'permission_denied'` when camera access is refused; permission is requested automatically.

In a browser, `capture` opens a file picker restricted to images. `width` and `height` are `0` there.

## Photos and video with the camera screen

`openCamera` opens the shell's own camera: viewfinder, lens switch, zoom, torch, shutter and a "use / retake" step all belong to the shell. Your page renders nothing and waits for the result, which keeps the capture experience the same in every app.

```tsx theme={null}
import { useCameraShot } from '@cskn/sdk';

function StoryComposer() {
  const { open, busy, available } = useCameraShot();

  if (available === false) {
    return <input type="file" accept="image/*,video/*" capture />;
  }

  const shoot = async () => {
    const shot = await open({
      mode: 'both',
      maxDurationMs: 60_000,
      title: 'New story',
      hint: 'Tap for a photo, hold to record',
    });
    if (shot.cancelled || shot.error) return;
    if (shot.photo) upload(shot.photo.uri);
    else if (shot.video?.blob) upload(shot.video.blob);
  };

  return <button onClick={shoot} disabled={busy || available === undefined}>Shoot</button>;
}
```

`available` starts `undefined` and settles once the shell answers. Mobile and desktop shells have a camera screen; TV and plain browsers do not.

### Options

<ParamField path="mode" type="'photo' | 'video' | 'both'" default="'photo'">
  `both` takes a photo on tap and records while the shutter is held. Video needs `'video'` or `'both'`, because the camera session has to be set up for it from the start.
</ParamField>

<ParamField path="facing" type="'back' | 'front'">
  The lens to start with. The user can still switch. On desktop this selects between webcams.
</ParamField>

<ParamField path="maxDurationMs" type="number">
  Recording limit, also shown as a countdown.
</ParamField>

<ParamField path="maxFileSize" type="number">
  Size limit in bytes.
</ParamField>

<ParamField path="title / hint" type="string">
  Text shown over the viewfinder.
</ParamField>

<ParamField path="allowGallery" type="boolean" default="true">
  Offer "pick from gallery" next to the shutter.
</ParamField>

<ParamField path="audio" type="boolean" default="true">
  Record sound in video mode. Requires microphone permission; pass `false` for silent clips.
</ParamField>

<ParamField path="deliver" type="'blob' | 'uri'" default="'blob'">
  `'uri'` skips transferring the video to the page and returns only the shell-side file path.
</ParamField>

### Result

<ResponseField name="photo" type="CameraAsset">
  Set when the user took a photo. `uri` is a base64 data URL.
</ResponseField>

<ResponseField name="video" type="object">
  <Expandable title="properties">
    <ResponseField name="url" type="string">A `blob:` URL. Works directly in `<video src>`.</ResponseField>
    <ResponseField name="blob" type="Blob">The bytes, for `FormData` or a resumable upload. Absent with `deliver: 'uri'`.</ResponseField>
    <ResponseField name="durationMs / size / mimeType" type="number / number / string">Clip details.</ResponseField>
    <ResponseField name="nativeUri" type="string">Shell-side path. Only useful handed back to the shell, for example to `shareFile`.</ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="cancelled" type="boolean">
  The user closed the camera without keeping anything.
</ResponseField>

<ResponseField name="error" type="string">
  `permission_denied`, `microphone_denied`, `not_available`, `busy`, `too_large` or `failed`.
</ResponseField>

<Note>
  A 60-second clip is roughly 10–20 MB and is transferred to the page in chunks, which can take a second or two. Clips over 64 MB are not transferred: you get `error: 'too_large'` and only `video.nativeUri`.
</Note>

Without hooks: `capturePhoto(options)`, `openCamera(options)` and `isCameraAvailable()`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.