# Time Chip
00–23시 또는 00–55분을 칩 그리드로 펼쳐 여러 개를 고르는 선택 컨트롤입니다. 드래그로 구간을 한 번에 고릅니다.
> 원본: `03_ASSET_GUIDE/catalog/time-chip.js` · CSS: `03_ASSET_GUIDE/styles/elements/time-chip.css`
> kind `component` · category `selection` · status `stable` · specSource `measured`
> 함께 쓰는 CSS: `chip.css`(칸 하나) · `button.css`(일괄 동작)
---
## 1. 구성
**Toolbar** 가 위, **Grid** 가 아래입니다. 그리드 한 칸은 **Item** 래퍼와 그 안의
**Chip** 두 겹으로 되어 있습니다.
```
┌──────────────────────────────────────────────────────────┐
│ 업무 시간 9 / 24 [전체 선택] [전체 해제] │ ← ① Toolbar
│ ② ③ ④ │
├──────────────────────────────────────────────────────────┤
│ (00)(01)(02)(03)(04)(05)(06)(07)(08)(09)(10)(11) │ ← ⑤ Grid
│ (12)(13)(14)(15)(16)(17)(18)(19)(20)(21)(22)(23) │
│ ⑥⑦ │
└──────────────────────────────────────────────────────────┘
```
| # | 요소 | 필수 | 클래스 | 설명 |
|---|---|---|---|---|
| — | Container | 필수 | `atom-time-chip` | 세로 flex. 드래그 중 `[data-dragging]`. |
| ① | Toolbar | 선택 | `atom-time-chip__toolbar` | 이름과 개수, 그리고 일괄 동작을 담습니다. 없어도 됩니다. |
| ② | Title | 선택 | `atom-time-chip__title` | 무엇을 고르는 그리드인지 말합니다. 선택 사항입니다. |
| ③ | Count | 선택 | `atom-time-chip__count` | `9 / 24`. 선택한 개수입니다. |
| ④ | Actions | 선택 | `atom-time-chip__actions` | 전체 선택 · 전체 해제. [Button](./button.md) 의 `xsmall` 입니다. |
| ⑤ | Grid | 필수 | `atom-time-chip__grid` | 값들이 열에 맞춰 놓이는 판입니다. |
| ⑥ | Item | 필수 | `atom-time-chip__item` | 칸 하나를 감싸는 래퍼입니다. **범위 드래그 상태를 가집니다.** |
| ⑦ | Chip | 필수 | `atom-chip atom-chip--type_checkbox` | [Chip](./chip.md) 의 `type_checkbox` 를 그대로 씁니다. |
> [!NOTE]
> **칩을 다시 정의하지 않습니다**
> 알약 모양·선택 채움·비활성 처리는 [Chip](./chip.md) 이 소유하므로 여기서 다시
> 정의하지 않습니다. 이 엘리먼트가 더하는 것은 **열에 맞춰 늘어놓기 · 일괄 선택 · 범위
> 드래그** 세 가지입니다.
>
> 범위 상태는 칩이 아니라 래퍼에 둡니다. 칩에 붙이면 이 엘리먼트에서만 쓰는 속성이 공용
> 엘리먼트에 추가되고, 칩의 배경은 선택 채움이 쓰고 있어 예고 링을 그릴 자리도 없습니다.
---
## 2. 종류 / 크기
### Unit
고르는 단위입니다. 값의 집합이 여기서 정해집니다.
| unit | 값 | 칸 수 |
| --- | --- | --- |
| `hour` | `00`–`23` | 24 — 12열 2줄 |
| `minute` | `00`–`55`, `step` 간격 | step 5 기준 12 — 12열 1줄 |
**둘을 한 그리드에 섞지 않습니다.** 시와 분을 함께 받아야 하면 그리드를 둘 두고 각각
이름을 답니다. 한 판에 섞으면 `09` 가 9시인지 9분인지 알 수 없습니다.
### Step
`minute` 에서만 씁니다. 시는 언제나 1시간 간격입니다.
| step | 칸 수 | 쓰는 곳 |
| --- | --- | --- |
| `5` | 12 | 대부분의 시간대 선택 (기본) |
| `10` | 6 | 느슨한 구간 |
| `15` | 4 | 15분 단위 예약 슬롯 |
| `30` | 2 | 30분 단위 예약 슬롯 |
칸이 줄면 **열 수도 함께 줄입니다.** 12열에 2칸만 놓으면 칩 두 개가 판 전체로 늘어나
버튼처럼 보입니다. [Grid 열](#grid-열) 을 보세요.
### Grid 열
`columns` 가 열 수를 정합니다. 기본은 **12** 입니다. 시(24)와 분(12) 모두 12로
나누어떨어지므로 같은 화면에 두 그리드를 놓아도 열이 맞습니다.
CSS 로는 `--atom-time-chip-cols` 커스텀 프로퍼티입니다.
```html
```
> [!NOTE]
> **`--atom-time-chip-cols` 는 토큰이 아닙니다**
> `tokens.css` 에 없습니다. 이 엘리먼트 안에서만 쓰이는 로컬 커스텀 프로퍼티이고,
> [Tree](./tree.md) 의 `--atom-tree-indent` · [Grid](./grid.md) 의
> `--atom-grid-tree-indent` 와 같은 성격입니다. `var(…, 12)` 로 폴백과 함께 읽습니다.
**열 수를 고정합니다.** 자유롭게 줄바꿈되게 두면 컨테이너 폭에 따라 `14` 가 매번
다른 열에 놓여 훑어보기 어렵습니다.
### 범위 드래그
한 칩을 누른 채 끌면 지나간 구간 전체가 바뀝니다. `07` 에서 눌러 `12` 까지
끌면 `07`–`12` 여섯 칸이 함께 선택됩니다.
| 단계 | 표시 | 어디에 |
| --- | --- | --- |
| 누른 순간 | 링 두 겹 (바깥 링에 간격) | `__item[data-anchor]` |
| 끄는 중 | 링 + 옅은 채움 | `__item[data-range]` |
| 손을 뗀 뒤 | 선택 채움 | 칩의 `__input:checked` |
**예고는 링, 선택은 채움으로 표시합니다.** 둘 다 채움으로 그리면 확정과 예고를 색 차이로만
구분해야 합니다. 링은 선택된 칩 위에도 같은 모양으로 얹히므로 두 상태가 섞이지 않습니다.
앵커 표시는 드래그가 끝날 때까지 남깁니다. 고정된 끝이 보이므로 반대 방향으로 끌어 구간을
줄일 수 있습니다.
#### 끌면 켜지나 꺼지나
**시작한 칩의 상태가 반대로 뒤집히고, 구간 전체가 그 상태를 따릅니다.**
- 꺼진 칩에서 시작 → 구간 전체가 켜집니다
- 켜진 칩에서 시작 → 구간 전체가 꺼집니다
한 방향으로만 동작하면 잘못 고른 구간을 지우려고 칩을 하나씩 눌러야 합니다.
### Size
| size | 칩 높이 | 칸 간격 | 쓰는 곳 |
| --- | --- | --- | --- |
| `medium` | 33px | `--atom-x2` (8px) | 시간대 선택이 화면의 주된 작업일 때 (기본) |
| `small` | 28px | 4px | 폼 안의 한 필드로 들어갈 때 |
높이는 [Chip](./chip.md) 이 소유합니다. `--size_small` 은 **간격만** 좁히고,
칩에는 `atom-chip--size_small` 을 함께 답니다.
### 클래스 계약
`styles/elements/time-chip.css` 헤더와 원본 `registry.json` `css.blocks` 기준입니다. 클래스가 없는 값(—)은 기본 스타일 그대로입니다.
| Prop | 값 | 클래스 |
|---|---|---|
| `size` | `small` · `medium` | `--size_small` · — (칩에는 `atom-chip--size_small` · —) |
| `columns` | `number` | 클래스 없음 — `__grid` 의 `style="--atom-time-chip-cols: N"` |
상태 선택자: `[data-dragging]`(Container) · `__item[data-range]` · `__item[data-anchor]` · 칩의 `__input:checked` · `[data-selected]` · `[aria-disabled="true"]`
---
## 3. 상태
칩의 상태를 그대로 씁니다. 이 엘리먼트가 더하는 상태는 범위 드래그 둘뿐입니다.
| 상태 | 표시 | 소유 |
| --- | --- | --- |
| 기본 | 약한 배경 | [Chip](./chip.md) |
| 선택 | 솔리드 채움 | [Chip](./chip.md) — `__input:checked` |
| 비활성 | 투명도 · 포인터 차단 | [Chip](./chip.md) — `aria-disabled="true"` |
| 범위 예고 | 링 + 옅은 채움 | 이 엘리먼트 — `__item[data-range]` |
| 범위 앵커 | 링 2겹 | 이 엘리먼트 — `__item[data-anchor]` |
**비활성 칩은 범위 드래그가 지나가도 바뀌지 않습니다.**
체크 글리프(`__check`)는 넣지 않습니다. 두 자리 숫자가 24칸 늘어선 판에서 글리프가
칸 폭의 절반을 차지하고, 선택은 채움만으로 구분됩니다.
드래그 중에는 컨테이너에 `data-dragging` 을 달아 글자 선택을 막습니다. 이 속성이 없으면
구간을 끌 때 숫자에 텍스트 선택 색이 칠해집니다.
---
## 4. Props
| Prop | Type | Default | 필수 | 설명 |
|---|---|---|---|---|
| `unit` | `"hour" \| "minute"` | `"hour"` | | 고르는 단위입니다. 값의 집합이 여기서 정해집니다. |
| `step` | `5 \| 10 \| 15 \| 30` | `5` | | 분 간격입니다. `unit: "minute"` 에서만 씁니다. |
| `value` | `number[]` | `[]` | | 선택된 값입니다. 시는 0–23, 분은 0–55. |
| `columns` | `number` | `12` | | 그리드 열 수입니다. |
| `disabledValues` | `number[]` | `[]` | | 고를 수 없는 값입니다. 범위 드래그가 지나가도 바뀌지 않습니다. |
| `rangeSelect` | `boolean` | `true` | | 드래그로 구간을 고릅니다. |
| `showBulkActions` | `boolean` | `true` | | 전체 선택 · 전체 해제 버튼을 둡니다. |
| `title` | `string` | | | Toolbar 의 이름입니다. 없으면 Toolbar 는 개수와 버튼만 담습니다. |
| `showCount` | `boolean` | `true` | | 이름 뒤에 `9 / 24` 를 붙입니다. |
| `size` | `"small" \| "medium"` | `"medium"` | | 칩 높이와 칸 간격입니다. |
| `label` | `string` | | ● | 그리드의 `aria-label` 입니다. |
| `onChange` | `(value: number[]) => void` | | | 선택이 바뀔 때 호출됩니다. 드래그는 **손을 뗀 시점에 한 번** 부릅니다. |
구현에서는 `value` 를 주면 controlled, 생략하면 `defaultValue`(기본 `[]`)에서 시작하는 uncontrolled 입니다.
`onChange` 로 넘기는 배열은 오름차순입니다. `title` · `showCount` · `showBulkActions` 가 모두 없으면 Toolbar 를 렌더하지 않습니다.
### Parts (compound)
| Part | 요소 | 설명 |
|---|---|---|
| `TimeChip` | `
` | 위 Props 를 받습니다. Toolbar(Title · Count · Actions) · Grid · Item · Chip 과 `aria-live` 알림 영역(`atom-sr-only`)을 모두 렌더합니다. 파트는 없습니다. |
---
## 5. Component tokens
| 대상 | 속성 | Token | Value |
| --- | --- | --- | --- |
| Container | gap | `{space.x3}` | 12px (small 은 `{space.x2}`) |
| Toolbar | border-bottom | `{color.border.subtle}` | 1px |
| Toolbar | padding-bottom | `{space.x2}` | 8px |
| Title | font | — | 13px (small 은 12px) · `{color.fg.neutral}` |
| Count | color | `{color.fg.neutral-muted}` | `tabular-nums` |
| Actions | gap | `{space.x2}` | 8px |
| Grid | gap | `{space.x2}` | 8px (small 은 4px) |
| Grid | 열 수 | — | `--atom-time-chip-cols` · 기본 12 (토큰 아님) |
| Item | radius | `{radius.full}` | 칩과 같은 모양으로 링이 앉게 |
| Item · range | ring | `{color.fg.brand}` | 2px |
| Item · range | 채움 | `{color.bg.side-active}` | 미선택 칸에만 · 브랜드 9% 틴트 |
| Item · anchor | outline | `{color.fg.brand}` | 2px · offset 2px (링 위에 한 겹 더) |
| Chip | 전부 | — | [Chip](./chip.md) 이 소유합니다 |
```json
{
"time-chip": {
"gap": {
"$type": "dimension",
"default": { "$value": "{space.x3}" },
"small": { "$value": "{space.x2}" }
},
"toolbar": {
"border": { "$type": "color", "$value": "{color.border.subtle}" },
"padding-bottom": { "$type": "dimension", "$value": "{space.x2}" }
},
"title": { "$type": "color", "$value": "{color.fg.neutral}" },
"count": { "$type": "color", "$value": "{color.fg.neutral-muted}" },
"actions-gap": { "$type": "dimension", "$value": "{space.x2}" },
"grid-gap": {
"$type": "dimension",
"default": { "$value": "{space.x2}" },
"small": { "$value": "4px" }
},
"item-radius": { "$type": "dimension", "$value": "{radius.full}" },
"range": {
"ring": { "$type": "color", "$value": "{color.fg.brand}" },
"fill": { "$type": "color", "$value": "{color.bg.side-active}" }
},
"anchor": { "$type": "color", "$value": "{color.fg.brand}" }
}
}
```
---
## 6. 접근성
- **역할** — 그리드는 `role="group"` 이고 `aria-label` 로 무엇을 고르는지 말합니다.
칩은 실제 `` 이므로 선택 상태가 그대로 전달됩니다.
- **이름** — 칩의 보이는 글자는 `09` 뿐입니다. 스크린리더에는 `aria-label` 로
`"09시"` · `"30분"` 처럼 단위를 붙여 읽힙니다. 숫자만으로는 시인지 분인지 알 수 없습니다.
- **키보드** — Tab 으로 칩 사이를 이동하고 Space 로 켜고 끕니다. **범위 선택에는
`Shift` + 클릭 또는 `Shift` + 화살표를 함께 제공해야 합니다.** 드래그는
포인터 전용 제스처라, 드래그만 있으면 키보드 사용자는 12칸을 하나씩 눌러야 합니다.
- **일괄 동작 알림** — 전체 선택·전체 해제와 범위 확정은 화면이 크게 바뀌는데 초점은
그대로입니다. `aria-live="polite"` 로 `"12개 선택됨"` 을 알립니다.
- **대비** — 선택 칩은 [Chip](./chip.md) 의 솔리드 채움이라 그쪽 기준을 따릅니다.
범위 예고 링은 `--atom-fg-brand` 이고, 색만으로 전달하지 않도록 **링(형태)** 과
**채움(명도)** 을 함께 씁니다. 채움은 `--atom-bg-side-active` 입니다.
`--atom-bg-informative-weak` 는 칩의 기본 배경과 6/255 밖에 차이가 나지 않아
실측에서 제외했습니다.
구현의 키보드 대안:
| 키 / 제스처 | 동작 |
| --- | --- |
| `Tab` | 칩 사이를 이동합니다(비활성 칩은 건너뜀) |
| `Space` | 지금 칩을 켜고 끕니다 |
| `Shift` + `←` `→` | 이웃 칩으로 초점을 옮기며 지금 칩의 상태(켜짐/꺼짐)를 이어 칠합니다 |
| `Shift` + `↑` `↓` | 한 줄(`columns` 칸) 위·아래로 옮기며 사이 구간 전체를 같은 상태로 칠합니다 |
| `Shift` + 클릭 | 직전에 바꾼 칩부터 누른 칩까지를 직전 상태로 칠합니다 |
---
## 7. 사용 가이드
### 전체 선택과 전체 해제는 할 일이 있을 때만 켭니다
0개 선택된 상태에서 "전체 해제" 는 눌러도 아무 동작이 없습니다. 동작이 없는 버튼은
비활성으로 둡니다.
- ✅ **Do** 0개면 전체 해제를, 전부면 전체 선택을 비활성으로 둡니다.
- ✅ **Do** Count 를 함께 둡니다. 24칸에서 선택 개수를 세기는 어렵습니다.
- ❌ **Don't** 전체 해제를 되돌릴 수 없게 만들지 않습니다. 잘못 누르면 선택이 모두 사라집니다.
### 열에 맞춰 놓습니다
같은 화면에 시 그리드와 분 그리드를 함께 놓을 때 둘의 열이 어긋나면 두 그리드가 서로
관련 없어 보입니다. 그래서 기본값이 12열입니다.
- ✅ **Do** 열 수를 정해 두고 값이 늘 같은 자리에 오게 합니다.
- ❌ **Don't** 컨테이너 폭에 따라 흐르게 두지 않습니다. `14` 가 매번 다른 열에 갑니다.
### 드래그 중에는 아직 확정이 아닙니다
드래그 중에 선택 채움을 칠하면 손을 떼기 전에 선택이 확정된 것처럼 보입니다. 방향을
되돌리면 칠했던 칸이 지워지는 것처럼 보여 조작 결과를 파악하기 어렵습니다.
- ✅ **Do** 예고는 링으로, 확정은 채움으로 구분합니다.
- ❌ **Don't** 드래그 중에 선택 채움을 미리 칠하지 않습니다.
### 고를 값의 범위가 넓으면 쓰지 않습니다
Time Chip 은 **값이 24개 이하로 정해져 있고 여러 개를 고르는** 화면에 씁니다.
- 임의의 시각 하나 → [Time Input](./time-input.md) 을 씁니다. 4번의 키 입력으로 끝납니다.
- 시작과 끝 두 값 → Time Input 두 개를 나란히 둡니다. 두 값만 고를 때는 24칸 그리드가 맞지 않습니다.
- 분 단위까지 임의로 → 칸이 60개가 되므로 입력 컴포넌트를 씁니다.
### 관련 컴포넌트
- [Chip](./chip.md) — 칸 하나입니다. 알약 모양·선택 채움·비활성은 Chip 이 정의합니다.
- [Time Input](./time-input.md) — 임의의 시각 **하나**를 넣을 때. 이쪽은 여러 개를 고를 때입니다.
- [Button](./button.md) — Toolbar 의 일괄 동작에 `xsmall` 로 씁니다.
- [Checkbox](./checkbox.md) — 값이 적고 이름이 긴 항목이면 칩 그리드보다 낫습니다.
---
## 8. 예시
```tsx
const [hours, setHours] = useState([8, 9, 10]);
```
```html