{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "bottom-sheet",
  "type": "registry:ui",
  "title": "BottomSheet",
  "description": "برگهٔ پایین",
  "registryDependencies": [
    "https://kashi.sepantapouya.com/r/cx.json",
    "https://kashi.sepantapouya.com/r/icons.json",
    "https://kashi.sepantapouya.com/r/kashi-provider.json",
    "https://kashi.sepantapouya.com/r/modal.json"
  ],
  "files": [
    {
      "path": "registry/kashi/BottomSheet.tsx",
      "type": "registry:ui",
      "target": "components/kashi/BottomSheet.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useId, useRef, useState, type KeyboardEvent as RKeyboardEvent, type PointerEvent as RPointerEvent, type ReactNode } from \"react\";\nimport { cx } from \"./cx\";\nimport { CloseIcon } from \"./icons\";\nimport { useKashi } from \"./KashiProvider\";\nimport { useModalDialog } from \"./modal\";\n\nexport interface BottomSheetProps {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n  title: ReactNode;\n  description?: ReactNode;\n  /** پنهان کردن عنوان از دید (صفحه‌خوان همچنان می‌خواند) */\n  hideTitle?: boolean;\n  /**\n   * ارتفاع‌های ایست به نسبت ارتفاع صفحه، از کوچک به بزرگ (مثل [0.5, 0.9]).\n   * بدون آن، برگه به اندازهٔ محتواست (حداکثر ۹۰٪) و کشیدن فقط برای بستن است.\n   */\n  snapPoints?: number[];\n  /** ایست اولیه (اندیس snapPoints) @default 0 */\n  defaultSnap?: number;\n  /** ایست فعلی (کنترل‌شده) */\n  snap?: number;\n  onSnapChange?: (index: number) => void;\n  /** دکمه‌های پایین برگه */\n  footer?: ReactNode;\n  /** بستن با کلیک روی پس‌زمینه و کشیدن به پایین @default true */\n  dismissible?: boolean;\n  /** دکمهٔ بستن در سربرگ @default false (دستگیره و Esc کافی‌اند) */\n  showClose?: boolean;\n  closeLabel?: string;\n  className?: string;\n  children?: ReactNode;\n}\n\n/** فاصله‌ای که بعد از آن، کشیدن به پایین برگه را می‌بندد (نسبت به ارتفاع برگه) */\nconst CLOSE_RATIO = 0.3;\n/** سرعت پرتاب (پیکسل بر میلی‌ثانیه) که بدون توجه به فاصله می‌بندد یا ایست را عوض می‌کند */\nconst FLING = 0.6;\n\n/**\n * برگهٔ پایین برای گوشی روی <dialog> بومی: با دستگیره بالا و پایین کشیده می‌شود،\n * روی ارتفاع‌های تعیین‌شده می‌ایستد و با کشیدن به پایین یا Esc بسته می‌شود.\n * دستگیره با کیبورد هم کار می‌کند: Enter ایست بعدی، جهت‌های بالا و پایین تغییر ارتفاع.\n */\nexport function BottomSheet({\n  open, onOpenChange, title, description, hideTitle, snapPoints, defaultSnap = 0, snap, onSnapChange,\n  footer, dismissible = true, showClose = false, closeLabel, className, children,\n}: BottomSheetProps) {\n  const { messages: M } = useKashi();\n  closeLabel ??= M.common.close;\n  const ref = useRef<HTMLDialogElement>(null);\n  const id = useId();\n  const { restoreFocus } = useModalDialog(ref, open);\n  const points = snapPoints?.length ? snapPoints : null;\n  const [innerSnap, setInnerSnap] = useState(defaultSnap);\n  const current = Math.min(snap ?? innerSnap, (points?.length ?? 1) - 1);\n  const drag = useRef<{ y: number; t: number; h: number; dy: number; v: number } | null>(null);\n\n  const setSnap = (i: number) => {\n    if (snap === undefined) setInnerSnap(i);\n    onSnapChange?.(i);\n  };\n\n  // هر بار باز شدن از ایست اولیه شروع شود\n  useEffect(() => {\n    if (open && snap === undefined) setInnerSnap(defaultSnap);\n  }, [open, snap, defaultSnap]);\n\n  const style = points ? ({ \"--k-sheet-height\": `${points[current] * 100}dvh` } as Record<string, string>) : undefined;\n\n  const onPointerDown = (e: RPointerEvent<HTMLElement>) => {\n    if (e.button !== 0 || (e.target as HTMLElement).closest(\".k-dialog__close\")) return;\n    const d = ref.current;\n    if (!d) return;\n    drag.current = { y: e.clientY, t: e.timeStamp, h: d.getBoundingClientRect().height, dy: 0, v: 0 };\n    (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);\n    d.setAttribute(\"data-dragging\", \"\");\n  };\n  const onPointerMove = (e: RPointerEvent<HTMLElement>) => {\n    const s = drag.current;\n    const d = ref.current;\n    if (!s || !d) return;\n    const dy = e.clientY - s.y;\n    const dt = Math.max(1, e.timeStamp - s.t);\n    s.v = (dy - s.dy) / dt;\n    s.dy = dy;\n    s.t = e.timeStamp;\n    // بالا کشیدن فقط تا بزرگ‌ترین ایست؛ بیشتر از آن با مقاومت\n    const maxUp = points ? (points[points.length - 1] * innerHeight) - s.h : 0;\n    const y = dy < 0 ? -Math.min(-dy, Math.max(0, maxUp)) - Math.max(0, -dy - Math.max(0, maxUp)) * 0.15 : dy;\n    d.style.translate = `0 ${y}px`;\n  };\n  const onPointerUp = () => {\n    const s = drag.current;\n    const d = ref.current;\n    drag.current = null;\n    if (!s || !d) return;\n    d.removeAttribute(\"data-dragging\");\n    d.style.translate = \"\";\n    const target = s.h - s.dy; // ارتفاع مطلوب کاربر\n    if (dismissible && (s.dy > s.h * CLOSE_RATIO || (s.v > FLING && s.dy > 24))) {\n      if (!points || current === 0 || s.v > FLING * 1.5 || target < points[0] * innerHeight * 0.6) {\n        d.style.translate = `0 ${Math.max(0, s.dy)}px`;\n        onOpenChange(false);\n        return;\n      }\n    }\n    if (!points) return;\n    let next = current;\n    if (Math.abs(s.v) > FLING) next = Math.max(0, Math.min(points.length - 1, current + (s.v < 0 ? 1 : -1)));\n    else next = points.reduce((best, p, i) => (Math.abs(p * innerHeight - target) < Math.abs(points[best] * innerHeight - target) ? i : best), 0);\n    if (next !== current) setSnap(next);\n  };\n\n  const onHandleKey = (e: RKeyboardEvent) => {\n    if (!points) return;\n    if (e.key === \"ArrowUp\" && current < points.length - 1) { e.preventDefault(); setSnap(current + 1); }\n    if (e.key === \"ArrowDown\" && current > 0) { e.preventDefault(); setSnap(current - 1); }\n  };\n  const expanded = points ? current === points.length - 1 : undefined;\n\n  return (\n    <dialog\n      ref={ref}\n      data-placement=\"bottom\"\n      className={cx(\"k-dialog k-dialog--bottom k-sheet\", points && \"k-sheet--snap\", className)}\n      style={style}\n      aria-labelledby={`${id}-title`}\n      aria-describedby={description ? `${id}-desc` : undefined}\n      onClose={() => {\n        if (ref.current) ref.current.style.translate = \"\";\n        if (open) onOpenChange(false);\n        restoreFocus();\n      }}\n      onCancel={(e) => {\n        e.preventDefault();\n        onOpenChange(false);\n      }}\n      onClick={(e) => {\n        if (dismissible && e.target === ref.current) onOpenChange(false);\n      }}\n    >\n      <div className=\"k-sheet__grab\" onPointerDown={onPointerDown} onPointerMove={onPointerMove} onPointerUp={onPointerUp} onPointerCancel={onPointerUp}>\n        {points ? (\n          <button\n            type=\"button\"\n            className=\"k-sheet__handle\"\n            aria-label={expanded ? M.sheet.collapse : M.sheet.expand}\n            aria-expanded={expanded}\n            onClick={() => setSnap(expanded ? 0 : current + 1)}\n            onKeyDown={onHandleKey}\n          />\n        ) : (\n          <span className=\"k-sheet__handle\" aria-hidden=\"true\" />\n        )}\n        <div className={cx(\"k-dialog__header k-sheet__header\", hideTitle && !showClose && \"k-sheet__header--hidden\")}>\n          <div className={cx(\"k-dialog__heading\", hideTitle && \"k-sr-only\")}>\n            <h2 className=\"k-dialog__title\" id={`${id}-title`}>{title}</h2>\n            {description && <p className=\"k-dialog__description\" id={`${id}-desc`}>{description}</p>}\n          </div>\n          {showClose && (\n            <button type=\"button\" className=\"k-btn k-btn--tertiary k-btn--icon k-btn--sm k-dialog__close\" aria-label={closeLabel} onClick={() => onOpenChange(false)}>\n              <CloseIcon />\n            </button>\n          )}\n        </div>\n      </div>\n      <div className=\"k-dialog__body k-sheet__body\">{children}</div>\n      {footer && <div className=\"k-dialog__footer k-sheet__footer\">{footer}</div>}\n    </dialog>\n  );\n}\n"
    }
  ]
}