# Bottom Sheet

`ShowtimeBottomSheet` 是一个面向移动端场景的底部弹层组件，支持可配置的吸附点列表，默认仍提供 `peek`、`half`、`full` 三档体验。

## 特性

- 默认提供 `peek / half / full` 三档配置
- 支持四段式和任意多段 snap 配置
- 拖拽和松手后会吸附到最近的 snap point
- 默认只允许顶部 header chrome 触发手势
- 内容区保留原生滚动
- 内容区到达边界后可继续推动 sheet 本身移动
- 支持轻微的首次入场上浮效果
- snap 切换支持更有阻尼感的 spring 动画
- 拖拽把手在按下时提供轻微的尺寸反馈
- 遵循 `prefers-reduced-motion`，关闭非必要的入场和把手动效
- 使用 `@vueuse/gesture` 处理拖拽
- 使用 `@vueuse/motion` 提供更顺滑的补间效果

## 基础用法

```vue
<script setup lang="ts">
import { ref } from 'vue'
import {
  ShowtimeBottomSheet,
  type ShowtimeBottomSheetDragPayload,
  type ShowtimeSheetSnap
} from 'showtime-components'

const snap = ref<ShowtimeSheetSnap>('peek')

function handleDrag(payload: ShowtimeBottomSheetDragPayload) {
  console.log('current sheet y:', payload.y)
}
</script>

<template>
  <ShowtimeBottomSheet
    v-model:snap="snap"
    @drag="handleDrag"
    :snap-points="[
      { key: 'peek', position: 88 },
      { key: 'quarter', position: 70 },
      { key: 'half', position: 50 },
      { key: 'full', position: 0 }
    ]"
  >
    <template #header>
      <strong>留言</strong>
    </template>
    <p>这里是内容区域</p>
  </ShowtimeBottomSheet>
</template>
```

## Props

| 名称 | 说明 | 默认值 |
| --- | --- | --- |
| `snap` | 当前吸附点 | `peek` |
| `snapPoints` | 推荐传有序数组 `[{ key, position }]`，兼容 `{ peek, half, full }` 对象写法 | `[{ peek: 90 }, { half: 50 }, { full: 1 }]` |
| `minDragDistance` | 触发切换的最小拖动距离 | `46` |
| `snapSettleTolerance` | 松手位置接近 snap 时直接吸附的容差 | `10` |
| `contentClass` | 面板附加类名 | `''` |
| `headerClass` | header 插槽容器附加类名 | `''` |
| `bodyClass` | 默认内容容器附加类名 | `''` |
| `entranceAnimation` | 是否启用首次入场动画 | `true` |
| `entranceOffset` | 首次入场上浮偏移，单位 px | `14` |
| `entranceDuration` | 首次入场动画时长，单位 ms | `260` |
| `snapAnimation` | 是否启用 snap 切换动画 | `true` |
| `snapAnimationStiffness` | snap 动画弹簧刚度 | `420` |
| `snapAnimationDamping` | snap 动画阻尼 | `36` |
| `snapAnimationMass` | snap 动画质量 | `0.9` |
| `headerDragOnly` | 是否只允许顶部区域触发拖拽；开启时会关闭内容区边界联动 | `true` |
| `handleVisible` | 是否显示默认把手 | `true` |
| `handleWidth` | 默认把手宽度，单位 px | `62` |
| `handleHeight` | 默认把手高度，单位 px | `8` |
| `panelBackground` | 面板背景色 | `#ffffff` |
| `panelRadius` | 面板顶部圆角，单位 px | `34` |
| `contentEdgeSnap` | 是否允许内容区在边界继续推动 sheet 连续移动（仅在 `headerDragOnly=false` 时生效） | `true` |
| `contentEdgeSnapThreshold` | 内容区边界切换的触发阈值 | `26` |

## 推荐配置

如果你希望维持当前默认体验，通常只需要：

```vue
<ShowtimeBottomSheet
  v-model:snap="snap"
  :entrance-animation="true"
  :snap-animation="true"
  :header-drag-only="true"
  panel-background="#ffffff"
/>
```

如果你希望显式配置四段 snap，可以这样写：

```vue
<ShowtimeBottomSheet
  v-model:snap="snap"
  :snap-points="[
    { key: 'peek', position: 88 },
    { key: 'quarter', position: 70 },
    { key: 'half', position: 50 },
    { key: 'full', position: 0 }
  ]"
/>
```

如果你要把拖拽区域扩展到整个面板，可以显式关闭 `headerDragOnly`：

```vue
<ShowtimeBottomSheet
  v-model:snap="snap"
  :header-drag-only="false"
/>
```

内容区可以放业务自己的内容容器。边界联动会从触摸目标向上查找真正可由用户滚动的容器，只有 `overflow-y: auto`、`scroll`、`overlay` 且存在纵向溢出的元素会参与滚动边界判断。

如果业务在非展开态使用 `overflow-y: hidden` 禁止内容滚动，这类内容不会被当成滚动容器，会被视为已经处在内容边界上，因此仍然可以通过内容区域拖动触发 sheet 的 snap 切换。

## 事件

| 名称 | 说明 |
| --- | --- |
| `update:snap` | `v-model:snap` 更新 |
| `snap-change` | 吸附点变化时触发 |
| `drag` | 顶部拖拽过程中持续触发，返回 `{ y, movementY, isDragging }` |
| `content-touchstart` | 内容区 `touchstart` 原生事件透传 |
| `content-touchmove` | 内容区 `touchmove` 原生事件透传 |
| `content-touchend` | 内容区 `touchend` 原生事件透传 |
| `content-touchcancel` | 内容区 `touchcancel` 原生事件透传 |
| `content-wheel` | 内容区 `wheel` 原生事件透传 |

## 事件监听示例

```vue
<script setup lang="ts">
import { ref } from 'vue'
import {
  ShowtimeBottomSheet,
  type ShowtimeBottomSheetDragPayload,
  type ShowtimeSheetSnap
} from 'showtime-components'

const snap = ref<ShowtimeSheetSnap>('peek')

function handleDrag(payload: ShowtimeBottomSheetDragPayload) {
  console.log(payload.y, payload.movementY, payload.isDragging)
}

function handleContentWheel(event: WheelEvent) {
  console.log('wheel delta:', event.deltaY)
}
</script>

<template>
  <ShowtimeBottomSheet
    v-model:snap="snap"
    @drag="handleDrag"
    @content-wheel="handleContentWheel"
  />
</template>
```

## 交互说明

- 默认挂载时会先直接落到当前 `snap`，不会先闪到 `peek`
- 首次渲染只做轻微上浮入场，不会做大幅滑入
- 手势默认只绑定在顶部拖拽区，因此内容区域可以自然滚动
- 内容区可以包含业务滚动容器，但只有 `overflow-y: auto/scroll/overlay` 的纵向溢出元素会参与边界判断
- `headerDragOnly=true` 时，内容区只保留原生滚动和事件透传，不会再把边界拖拽升级成 sheet 位移
- 直接拖拽时，面板会连续跟手移动
- `drag` 事件会同步给出当前面板相对视口顶部的 y 轴像素坐标
- 松手后会吸附到最近的已配置 snap point
- `snapPoints` 不再限制为三档，数组里的每一段都可以成为最终停靠点
- 按下或拖拽顶部把手时，把手会轻微变宽并增强对比度，用于确认当前手势已被接收
- 当 `contentEdgeSnap` 开启时，内容区滚到边界后继续拖动，会把后续位移转交给 sheet 本身
- 当业务内容为 `overflow-y: hidden` 时，组件会把它视为不可用户滚动内容，内容区拖动仍可触发 snap
- 内容区边界联动不再直接“切到下一档”，而是先连续移动，再在松手时吸附到最近点
- 内容区的 `touchstart`、`touchmove`、`touchend`、`touchcancel`、`wheel` 都会继续向外透传，便于业务侧做埋点、联动和自定义控制
- 旧版 `{ peek, half, full }` 对象写法仍可用，但推荐改用数组形式
- 如果系统开启 `prefers-reduced-motion`，首次入场动画会自动关闭
- 如果系统开启 `prefers-reduced-motion: reduce`，把手的缩放反馈和非必要过渡也会关闭，拖拽与吸附逻辑不受影响

## 样式定制

默认文字、把手、投影和通用动效可以通过 [样式定制](./style-customization.md) 中的共享 token 覆盖。`panelBackground` 与 `panelRadius` 仍优先使用当前实例传入的 prop。
