---
title: "Drag コンポーネント — zenit Zig UI"
description: "ドラッグと水平方向の resize ハンドル。pointer capture、しきい値、grab / grabbing カーソル、クリック抑制は ui.interaction…"
url: https://zenit.z.express/ja/components/drag
language: ja
alternate_en: https://zenit.z.express/components/drag.md
alternate_zh: https://zenit.z.express/zh/components/drag.md
alternate_es: https://zenit.z.express/es/components/drag.md
alternate_ko: https://zenit.z.express/ko/components/drag.md
alternate_fr: https://zenit.z.express/fr/components/drag.md
alternate_de: https://zenit.z.express/de/components/drag.md
project: zenit v0.1.0-alpha (Zig 0.15.2, macOS)
source: https://github.com/version-next/zenit
---

# Drag

`ui.interaction`

ドラッグと水平方向の resize ハンドル。

[Video](https://zenit.z.express/media/stories/drag.mp4?v=224e651da9)

**BEHAVIOR CONTRACT**

pointer capture、しきい値、grab / grabbing カーソル、クリック抑制は ui.interaction が一括で処理します。

**RECORDED INTERACTION**

harness がボックスをドラッグし、状態が idle → dragging に変わります。

`examples/storybook/stories.zig:4264`

```zig
pub fn buildDrag(scope: *ui.Scope, cx: *ui.Cx) anyerror!*ui.Node {
    const a = cx.allocator;
    const c = try col(cx, 12);
    try c.appendChild(a, try label(cx, "Drag — ui.interaction.drag primitive (threshold 4px, Escape cancels, arrows nudge when focused)"));

    // ── 1. 自由移动方块（both 轴 + click 计数 + 键盘等价）──
    const arena = try ui.box(cx, .{
        .width = .{ .px = 420 },
        .height = .{ .px = 180 },
        .background = light.color.bg_secondary,
    }, .{});
    (try arena.style.ensureExtFallible(a)).corner_radius = ui.CornerRadius.uniform(12);

    const subject = try ui.box(cx, .{
        .width = .{ .px = 72 },
        .height = .{ .px = 72 },
        .background = light.color.accent,
        .align_items = .center,
        .justify = .center,
    }, .{});
    (try subject.style.ensureExtFallible(a)).corner_radius = ui.CornerRadius.uniform(10);
    subject.style.cursor = .grab;
    subject.meta.ownership.meta.test_id = "story.drag.box";
    subject.setFocusable(true);
    try subject.appendChild(a, try ui.text(cx, "drag", .{ .font_size = 13, .color = ui.Color.rgba(255, 255, 255, 255) }));
    try arena.appendChild(a, subject);

    const status_label = try ui.text(cx, "state: idle", .{ .font_size = 13, .color = light.color.fg_primary });
    status_label.meta.ownership.meta.test_id = "story.drag.status";
    const clicks_label = try ui.text(cx, "clicks: 0 (drag suppresses click)", .{ .font_size = 13, .color = light.color.fg_primary });
    clicks_label.meta.ownership.meta.test_id = "story.drag.clicks";

    // ── 2. 水平 resize handle ──
    const resize_row = try row(cx, 0);
    const panel = try ui.box(cx, .{
        .width = .{ .px = 240 },
        .height = .{ .px = 80 },
        .background = light.color.bg_tertiary,
        .align_items = .center,
        .justify = .center,
    }, .{});
    panel.meta.ownership.meta.test_id = "story.drag.panel";
    (try panel.style.ensureExtFallible(a)).corner_radius = ui.CornerRadius.uniform(8);
    const width_label = try ui.text(cx, "width: 240px", .{ .font_size = 13, .color = light.color.fg_primary });
    width_label.meta.ownership.meta.test_id = "story.drag.width";
    try panel.appendChild(a, width_label);

    const handle = try ui.box(cx, .{
        .width = .{ .px = 10 },
        .height = .{ .px = 80 },
        .background = light.color.border,
    }, .{});
    handle.style.cursor = .col_resize;
    handle.meta.ownership.meta.test_id = "story.drag.handle";
    try resize_row.appendChild(a, panel);
    try resize_row.appendChild(a, handle);

    const st = try cx.bindState(DragStory, .{
        .cx = cx,
        .subject = subject,
        .status_label = status_label,
        .clicks_label = clicks_label,
        .panel = panel,
        .width_label = width_label,
    });

    _ = try ui.interaction.drag.Binding.attach(scope, cx, subject, .{
        .activation_distance = 4,
        .active_cursor = .grabbing,
    }, DragStory.onMove, st);
    _ = try ui.interaction.drag.Binding.attach(scope, cx, handle, .{
        .axis = .horizontal,
        .active_cursor = .col_resize,
    }, DragStory.onResize, st);

    // click 计数走独立的 on_click 槽（drag binding 占用 on_event，两者可组合；
    // 越阈值/取消后 dispatcher 会抑制 click —— 计数不会涨）
    subject.behavior.events.on_click = .{ .callback = DragStory.onClick, .context = @ptrCast(st) };
    // 键盘等价操作走独立的 on_key_down 槽（key_context 与 event_context 分离）
    subject.behavior.events.on_key_down = DragStory.onKeyDown;
    subject.behavior.events.key_context = @ptrCast(st);

    try c.appendChild(a, arena);
    try c.appendChild(a, status_label);
    try c.appendChild(a, clicks_label);
    try c.appendChild(a, try label(cx, "Horizontal resize — drag the handle; delta is axis-projected"));
    try c.appendChild(a, resize_row);
    return c;
}
```

[GitHub でソースを見るexamples/storybook/stories.zig:4264](https://github.com/version-next/zenit/blob/HEAD/examples/storybook/stories.zig#L4264)

Storybook にあるこの story のコードそのままです（zenit 5f9add5+wip 2026-09-30）。`W` は `ui.widgets` のことで、`col` / `row` / `label` は Storybook 内の小さなレイアウトヘルパーです。
