{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "carousel",
  "type": "registry:ui",
  "title": "Carousel",
  "description": "کاروسل",
  "registryDependencies": [
    "https://kashi.sepantapouya.com/r/cx.json",
    "https://kashi.sepantapouya.com/r/kashi-provider.json"
  ],
  "files": [
    {
      "path": "registry/kashi/Carousel.tsx",
      "type": "registry:ui",
      "target": "components/kashi/Carousel.tsx",
      "content": "\"use client\";\n\nimport { Children, useEffect, useId, useRef, useState, type CSSProperties, type PointerEvent as ReactPointerEvent, type ReactNode } from \"react\";\nimport { cx } from \"./cx\";\nimport { useKashi } from \"./KashiProvider\";\n\nexport interface CarouselProps {\n  /** هر فرزند یک اسلاید */\n  children: ReactNode;\n  /** نام اسلایدر، مثل «محصولات پیشنهادی» (الزامی) */\n  label: string;\n  /** تعداد اسلاید در هر نما؛ با effect=\"fade\" همیشه ۱ @default 1 */\n  perView?: number;\n  /** نمایش نقطه‌ها @default true */\n  dots?: boolean;\n  /**\n   * «slide» ریل اسکرول بومی؛ «fade» محو شدن اسلایدها روی هم با opacity، برای عکس‌های بزرگ\n   * (Safari لایه‌های بزرگِ دارای transform را تکه‌تکه و تکراری رسم می‌کند). کشیدن انگشت در هر دو کار می‌کند. @default \"slide\"\n   */\n  effect?: \"slide\" | \"fade\";\n  /**\n   * پخش خودکار؛ true یعنی هر ۵ ثانیه، عدد یعنی فاصله به میلی‌ثانیه. با hover، فوکوس، پنهان شدن زبانه و نمای بزرگ مکث می‌کند،\n   * دکمهٔ توقف دارد (WCAG 2.2.2) و با prefers-reduced-motion متوقف شروع می‌شود.\n   */\n  autoplay?: boolean | number;\n  /** نوار تصویرهای کوچک زیر اسلایدر؛ یکی برای هر اسلاید، معمولاً <img> کوچک */\n  thumbnails?: ReactNode[];\n  /** دکمهٔ «نمایش بزرگ» و باز شدن اسلاید با کلیک در پنجرهٔ تمام‌صفحه (Esc، کلیدهای جهت، فوکوس داخل پنجره) */\n  lightbox?: boolean;\n  /** بعد از عوض شدن اسلاید */\n  onIndexChange?: (index: number) => void;\n  className?: string;\n  labels?: { prev?: string; next?: string; slide?: (n: number, total: number) => string; play?: string; pause?: string; expand?: string };\n}\n\nconst Arrow = ({ next }: { next?: boolean }) => (\n  <svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" strokeWidth={1.8} strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\" data-k-mirror=\"\">\n    <path d={next ? \"m8 5 5 5-5 5\" : \"m12 5-5 5 5 5\"} />\n  </svg>\n);\nconst PlayIcon = ({ playing }: { playing: boolean }) => (\n  <svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" strokeWidth={1.8} strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n    {playing ? <path d=\"M7.5 5v10M12.5 5v10\" /> : <path d=\"M7 4.8v10.4a.6.6 0 0 0 .9.5l8-5.2a.6.6 0 0 0 0-1L7.9 4.3a.6.6 0 0 0-.9.5Z\" />}\n  </svg>\n);\nconst ExpandIcon = () => (\n  <svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" strokeWidth={1.8} strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\"><path d=\"M12 3.5h4.5V8M8 16.5H3.5V12M16.5 3.5 11.5 8.5M3.5 16.5l5-5\" /></svg>\n);\nconst CloseIcon = () => (\n  <svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" strokeWidth={1.8} strokeLinecap=\"round\" aria-hidden=\"true\"><path d=\"m5.5 5.5 9 9m0-9-9 9\" /></svg>\n);\n\n/** inert در React 18 و 19 یکسان نوشته نمی‌شود؛ مستقیم روی DOM */\nconst inert = (on: boolean) => (el: HTMLElement | null) => {\n  if (el) on ? el.setAttribute(\"inert\", \"\") : el.removeAttribute(\"inert\");\n};\n\nconst isRtl = (el: Element) => getComputedStyle(el).direction === \"rtl\";\n\n/** کشیدن انگشت یا قلم: در RTL اسلاید بعدی سمت چپ است، پس کشیدن به راست یعنی «بعدی» */\nfunction useSwipe(onStep: (step: number) => void) {\n  const start = useRef<{ x: number; y: number } | null>(null);\n  return {\n    onPointerDown: (e: ReactPointerEvent<HTMLElement>) => {\n      if (e.pointerType === \"mouse\") return;\n      start.current = { x: e.clientX, y: e.clientY };\n    },\n    onPointerUp: (e: ReactPointerEvent<HTMLElement>) => {\n      const s = start.current;\n      start.current = null;\n      if (!s) return;\n      const dx = e.clientX - s.x, dy = e.clientY - s.y;\n      if (Math.abs(dx) < 40 || Math.abs(dx) < Math.abs(dy) * 1.2) return;\n      onStep((dx < 0) !== isRtl(e.currentTarget) ? 1 : -1);\n    },\n    onPointerCancel: () => {\n      start.current = null;\n    },\n  };\n}\n\n/**\n * اسلایدر روی scroll-snap بومی (یا محو شدن با effect=\"fade\"): کشیدن، چرخ موس، دکمه‌ها و کلیدهای جهت.\n * اختیاری: تصویرهای کوچک، پخش خودکار با نوار پیشرفت و دکمهٔ توقف، و نمای بزرگ تمام‌صفحه.\n */\nexport function Carousel({\n  children, label, perView: perViewProp = 1, dots = true, effect = \"slide\", autoplay, thumbnails, lightbox, onIndexChange, className, labels = {},\n}: CarouselProps) {\n  const id = useId();\n  const { messages: M, n: N } = useKashi();\n  const {\n    prev = M.common.prev, next = M.common.next, slide = (a: number, t: number) => M.carousel.slideOf(N(a), N(t)),\n    play = M.carousel.play, pause = M.carousel.pause, expand = M.carousel.expand,\n  } = labels;\n  const fade = effect === \"fade\";\n  const perView = fade ? 1 : perViewProp;\n  const slides = Children.toArray(children);\n  const track = useRef<HTMLDivElement>(null);\n  const thumbs = useRef<HTMLDivElement>(null);\n  const dialog = useRef<HTMLDialogElement>(null);\n  const [index, setIndexState] = useState(0);\n  const [boxIndex, setBoxIndex] = useState<number | null>(null);\n  const pages = Math.max(1, slides.length - perView + 1);\n\n  const setIndex = (i: number) => {\n    setIndexState((old) => {\n      if (old !== i) onIndexChange?.(i);\n      return i;\n    });\n  };\n\n  // اسلاید فعال از روی موقعیت اسکرول (فقط در حالت slide)\n  useEffect(() => {\n    const el = track.current;\n    if (!el || fade || typeof IntersectionObserver === \"undefined\") return;\n    const items = [...el.children] as HTMLElement[];\n    const io = new IntersectionObserver(\n      (entries) => {\n        const vis = entries.filter((e) => e.isIntersecting && e.intersectionRatio > 0.6).map((e) => items.indexOf(e.target as HTMLElement));\n        if (vis.length) setIndex(Math.min(...vis));\n      },\n      { root: el, threshold: [0.6, 1] },\n    );\n    items.forEach((s) => io.observe(s));\n    return () => io.disconnect();\n  }, [slides.length, fade]);\n\n  const go = (i: number, wrap = false) => {\n    const n = wrap ? (i + pages) % pages : Math.max(0, Math.min(pages - 1, i));\n    if (!fade) {\n      const t = track.current;\n      const el = t?.children[n] as HTMLElement | undefined;\n      if (!el || !t) return;\n      // scrollIntoView صفحه را هم اسکرول می‌کند؛ فقط خود ریل را جابه‌جا می‌کنیم\n      const offset = isRtl(t) ? -(t.getBoundingClientRect().right - el.getBoundingClientRect().right) + t.scrollLeft : el.getBoundingClientRect().left - t.getBoundingClientRect().left + t.scrollLeft;\n      t.scrollTo?.({ left: offset });\n    }\n    setIndex(n);\n  };\n\n  // تصویر کوچک فعال در نوار خودش دیده شود (بدون اسکرول صفحه)\n  useEffect(() => {\n    const strip = thumbs.current;\n    const b = strip?.children[index] as HTMLElement | undefined;\n    if (!strip || !b) return;\n    const s = strip.getBoundingClientRect(), r = b.getBoundingClientRect();\n    if (r.left < s.left || r.right > s.right) strip.scrollTo?.({ left: strip.scrollLeft + (r.left + r.width / 2) - (s.left + s.width / 2) });\n  }, [index]);\n\n  // ---- پخش خودکار\n  const interval = autoplay === true ? 5000 : typeof autoplay === \"number\" && autoplay > 0 ? autoplay : 0;\n  const [playing, setPlaying] = useState(interval > 0);\n  const [hold, setHold] = useState({ hover: false, focus: false, hidden: false });\n  useEffect(() => {\n    if (!interval) return;\n    if (matchMedia?.(\"(prefers-reduced-motion: reduce)\").matches) setPlaying(false);\n    const onVis = () => setHold((h) => ({ ...h, hidden: document.hidden }));\n    document.addEventListener(\"visibilitychange\", onVis);\n    return () => document.removeEventListener(\"visibilitychange\", onVis);\n  }, [interval]);\n  const running = interval > 0 && pages > 1 && playing && !hold.hover && !hold.focus && !hold.hidden && boxIndex === null;\n  const left = useRef(interval);\n  const startedAt = useRef(0);\n  useEffect(() => {\n    left.current = interval;\n  }, [index, interval]);\n  useEffect(() => {\n    if (!running) return;\n    startedAt.current = Date.now();\n    const t = setTimeout(() => go(index + 1, true), left.current);\n    return () => {\n      clearTimeout(t);\n      left.current = Math.max(0, left.current - (Date.now() - startedAt.current));\n    };\n  }, [running, index]);\n\n  // ---- نمای بزرگ\n  useEffect(() => {\n    const d = dialog.current as (HTMLDialogElement & { showModal?: () => void }) | null;\n    if (!d) return;\n    if (boxIndex !== null && !d.open) d.showModal?.();\n    if (boxIndex === null && d.open) d.close();\n  }, [boxIndex !== null]);\n  const openBox = (i: number) => lightbox && setBoxIndex(i);\n  const boxGo = (i: number) => setBoxIndex((i + slides.length) % slides.length);\n  const closeBox = () => {\n    if (boxIndex !== null) go(Math.min(boxIndex, pages - 1));\n    setBoxIndex(null);\n  };\n\n  const swipe = useSwipe((step) => go(index + step));\n  const boxSwipe = useSwipe((step) => boxIndex !== null && boxGo(boxIndex + step));\n\n  return (\n    <section\n      className={cx(\"k-carousel\", fade && \"k-carousel--fade\", className)}\n      aria-roledescription={M.carousel.slider}\n      aria-label={label}\n      data-lightbox={lightbox ? \"\" : undefined}\n      style={{ \"--k-carousel-per-view\": perView } as CSSProperties}\n      onMouseEnter={() => setHold((h) => ({ ...h, hover: true }))}\n      onMouseLeave={() => setHold((h) => ({ ...h, hover: false }))}\n      onFocus={() => setHold((h) => ({ ...h, focus: true }))}\n      onBlur={(e) => !e.currentTarget.contains(e.relatedTarget as Node) && setHold((h) => ({ ...h, focus: false }))}\n    >\n      <div\n        ref={track}\n        className=\"k-carousel__track\"\n        tabIndex={0}\n        // در پخش خودکار، اعلام هر اسلاید صفحه‌خوان را خسته می‌کند\n        aria-live={running ? \"off\" : \"polite\"}\n        {...(fade ? swipe : {})}\n        onKeyDown={(e) => {\n          const rtl = isRtl(e.currentTarget);\n          const step = { ArrowLeft: rtl ? 1 : -1, ArrowRight: rtl ? -1 : 1 }[e.key];\n          if (step) {\n            e.preventDefault();\n            go(index + step);\n          } else if (e.key === \"Home\") go(0);\n          else if (e.key === \"End\") go(pages - 1);\n          else if (lightbox && e.key === \"Enter\" && e.target === e.currentTarget) openBox(index);\n        }}\n      >\n        {slides.map((s, i) => {\n          const off = fade && i !== index;\n          return (\n            <div\n              key={i}\n              className=\"k-carousel__slide\"\n              role=\"group\"\n              aria-roledescription={M.carousel.slide}\n              aria-label={slide(i + 1, slides.length)}\n              id={`${id}-s${i}`}\n              data-active={fade ? (i === index ? \"\" : undefined) : undefined}\n              aria-hidden={off || undefined}\n              ref={inert(off)}\n              onClick={lightbox ? (e) => !(e.target as Element).closest(\"a, button, input, select, textarea, [tabindex]\") && openBox(i) : undefined}\n            >\n              {s}\n            </div>\n          );\n        })}\n      </div>\n      {interval > 0 && pages > 1 && (\n        <span className=\"k-carousel__progress\" aria-hidden=\"true\">\n          <span key={`${index}-${playing}`} className=\"k-carousel__progress-bar\" data-paused={running ? undefined : \"\"} style={{ \"--_d\": `${interval}ms` } as CSSProperties} />\n        </span>\n      )}\n      {thumbnails && thumbnails.length > 0 && (\n        <div ref={thumbs} className=\"k-carousel__thumbs\" role=\"group\" aria-label={M.carousel.thumbnails(label)}>\n          {thumbnails.map((t, i) => (\n            <button\n              key={i}\n              type=\"button\"\n              className=\"k-carousel__thumb\"\n              aria-label={slide(i + 1, slides.length)}\n              aria-current={Math.min(i, pages - 1) === index && (perView === 1 || i === index)}\n              aria-controls={`${id}-s${i}`}\n              onClick={() => go(i)}\n            >\n              {t}\n            </button>\n          ))}\n        </div>\n      )}\n      {(pages > 1 || lightbox) && (\n        <div className=\"k-carousel__bar\">\n          {dots && pages > 1 ? (\n            <ul className=\"k-carousel__dots\" aria-label={M.carousel.slides(label)}>\n              {Array.from({ length: pages }, (_, i) => (\n                <li key={i}>\n                  <button type=\"button\" className=\"k-carousel__dot\" aria-label={slide(i + 1, pages)} aria-current={i === index} aria-controls={`${id}-s${i}`} onClick={() => go(i)} />\n                </li>\n              ))}\n            </ul>\n          ) : <span />}\n          <div className=\"k-carousel__nav\">\n            {interval > 0 && pages > 1 && (\n              <button type=\"button\" className=\"k-btn k-btn--tertiary k-btn--icon k-btn--sm\" aria-label={playing ? pause : play} aria-pressed={!playing} onClick={() => setPlaying((p) => !p)}>\n                <PlayIcon playing={playing} />\n              </button>\n            )}\n            {lightbox && (\n              <button type=\"button\" className=\"k-btn k-btn--tertiary k-btn--icon k-btn--sm\" aria-label={expand} aria-haspopup=\"dialog\" onClick={() => openBox(index)}><ExpandIcon /></button>\n            )}\n            {pages > 1 && (\n              <>\n                <button type=\"button\" className=\"k-btn k-btn--secondary k-btn--icon k-btn--sm\" aria-label={prev} disabled={index === 0} onClick={() => go(index - 1)}><Arrow /></button>\n                <button type=\"button\" className=\"k-btn k-btn--secondary k-btn--icon k-btn--sm\" aria-label={next} disabled={index >= pages - 1} onClick={() => go(index + 1)}><Arrow next /></button>\n              </>\n            )}\n          </div>\n        </div>\n      )}\n      {lightbox && (\n        <dialog\n          ref={dialog}\n          className=\"k-carousel__lightbox\"\n          aria-label={M.carousel.lightbox(label)}\n          onClose={closeBox}\n          onCancel={(e) => {\n            e.preventDefault();\n            closeBox();\n          }}\n          onClick={(e) => e.target === e.currentTarget && closeBox()}\n          onKeyDown={(e) => {\n            if (boxIndex === null) return;\n            const rtl = isRtl(e.currentTarget);\n            const step = { ArrowLeft: rtl ? 1 : -1, ArrowRight: rtl ? -1 : 1 }[e.key];\n            if (step) {\n              e.preventDefault();\n              boxGo(boxIndex + step);\n            }\n          }}\n        >\n          {boxIndex !== null && (\n            <>\n              <div className=\"k-carousel__lightbox-head\">\n                <span className=\"k-carousel__lightbox-count\" aria-live=\"polite\">{slide(boxIndex + 1, slides.length)}</span>\n                <button type=\"button\" className=\"k-btn k-btn--tertiary k-btn--icon\" aria-label={M.common.close} onClick={closeBox} autoFocus><CloseIcon /></button>\n              </div>\n              <div className=\"k-carousel__lightbox-stage\" {...boxSwipe}>\n                {slides.map((s, i) => (\n                  <div key={i} className=\"k-carousel__lightbox-slide\" data-active={i === boxIndex ? \"\" : undefined} aria-hidden={i !== boxIndex || undefined} ref={inert(i !== boxIndex)}>\n                    {s}\n                  </div>\n                ))}\n              </div>\n              {slides.length > 1 && (\n                <div className=\"k-carousel__lightbox-nav\">\n                  <button type=\"button\" className=\"k-btn k-btn--secondary k-btn--icon\" aria-label={prev} onClick={() => boxGo(boxIndex - 1)}><Arrow /></button>\n                  <button type=\"button\" className=\"k-btn k-btn--secondary k-btn--icon\" aria-label={next} onClick={() => boxGo(boxIndex + 1)}><Arrow next /></button>\n                </div>\n              )}\n            </>\n          )}\n        </dialog>\n      )}\n    </section>\n  );\n}\n"
    }
  ]
}