@summa-ui/surah-picker 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1084 @@
1
+ // src/core/SurahWheelPicker.tsx
2
+ import { useEffect, useRef, useState, useCallback } from "react";
3
+
4
+ // src/data/quran.ts
5
+ var SURAH_LIST = [
6
+ { number: 1, name: "Al-Fatihah", arabic: "\u0627\u0644\u0641\u0627\u062A\u062D\u0629", ayatCount: 7 },
7
+ { number: 2, name: "Al-Baqarah", arabic: "\u0627\u0644\u0628\u0642\u0631\u0629", ayatCount: 286 },
8
+ { number: 3, name: "Ali Imran", arabic: "\u0622\u0644 \u0639\u0645\u0631\u0627\u0646", ayatCount: 200 },
9
+ { number: 4, name: "An-Nisa", arabic: "\u0627\u0644\u0646\u0633\u0627\u0621", ayatCount: 176 },
10
+ { number: 5, name: "Al-Maidah", arabic: "\u0627\u0644\u0645\u0627\u0626\u062F\u0629", ayatCount: 120 },
11
+ { number: 6, name: "Al-An'am", arabic: "\u0627\u0644\u0623\u0646\u0639\u0627\u0645", ayatCount: 165 },
12
+ { number: 7, name: "Al-A'raf", arabic: "\u0627\u0644\u0623\u0639\u0631\u0627\u0641", ayatCount: 206 },
13
+ { number: 8, name: "Al-Anfal", arabic: "\u0627\u0644\u0623\u0646\u0641\u0627\u0644", ayatCount: 75 },
14
+ { number: 9, name: "At-Taubah", arabic: "\u0627\u0644\u062A\u0648\u0628\u0629", ayatCount: 129 },
15
+ { number: 10, name: "Yunus", arabic: "\u064A\u0648\u0646\u0633", ayatCount: 109 },
16
+ { number: 11, name: "Hud", arabic: "\u0647\u0648\u062F", ayatCount: 123 },
17
+ { number: 12, name: "Yusuf", arabic: "\u064A\u0648\u0633\u0641", ayatCount: 111 },
18
+ { number: 13, name: "Ar-Ra'd", arabic: "\u0627\u0644\u0631\u0639\u062F", ayatCount: 43 },
19
+ { number: 14, name: "Ibrahim", arabic: "\u0625\u0628\u0631\u0627\u0647\u064A\u0645", ayatCount: 52 },
20
+ { number: 15, name: "Al-Hijr", arabic: "\u0627\u0644\u062D\u062C\u0631", ayatCount: 99 },
21
+ { number: 16, name: "An-Nahl", arabic: "\u0627\u0644\u0646\u062D\u0644", ayatCount: 128 },
22
+ { number: 17, name: "Al-Isra", arabic: "\u0627\u0644\u0625\u0633\u0631\u0627\u0621", ayatCount: 111 },
23
+ { number: 18, name: "Al-Kahf", arabic: "\u0627\u0644\u0643\u0647\u0641", ayatCount: 110 },
24
+ { number: 19, name: "Maryam", arabic: "\u0645\u0631\u064A\u0645", ayatCount: 98 },
25
+ { number: 20, name: "Ta-Ha", arabic: "\u0637\u0647", ayatCount: 135 },
26
+ { number: 21, name: "Al-Anbiya", arabic: "\u0627\u0644\u0623\u0646\u0628\u064A\u0627\u0621", ayatCount: 112 },
27
+ { number: 22, name: "Al-Hajj", arabic: "\u0627\u0644\u062D\u062C", ayatCount: 78 },
28
+ { number: 23, name: "Al-Mu'minun", arabic: "\u0627\u0644\u0645\u0624\u0645\u0646\u0648\u0646", ayatCount: 118 },
29
+ { number: 24, name: "An-Nur", arabic: "\u0627\u0644\u0646\u0648\u0631", ayatCount: 64 },
30
+ { number: 25, name: "Al-Furqan", arabic: "\u0627\u0644\u0641\u0631\u0642\u0627\u0646", ayatCount: 77 },
31
+ { number: 26, name: "Ash-Shu'ara", arabic: "\u0627\u0644\u0634\u0639\u0631\u0627\u0621", ayatCount: 227 },
32
+ { number: 27, name: "An-Naml", arabic: "\u0627\u0644\u0646\u0645\u0644", ayatCount: 93 },
33
+ { number: 28, name: "Al-Qasas", arabic: "\u0627\u0644\u0642\u0635\u0635", ayatCount: 88 },
34
+ { number: 29, name: "Al-Ankabut", arabic: "\u0627\u0644\u0639\u0646\u0643\u0628\u0648\u062A", ayatCount: 69 },
35
+ { number: 30, name: "Ar-Rum", arabic: "\u0627\u0644\u0631\u0648\u0645", ayatCount: 60 },
36
+ { number: 31, name: "Luqman", arabic: "\u0644\u0642\u0645\u0627\u0646", ayatCount: 34 },
37
+ { number: 32, name: "As-Sajdah", arabic: "\u0627\u0644\u0633\u062C\u062F\u0629", ayatCount: 30 },
38
+ { number: 33, name: "Al-Ahzab", arabic: "\u0627\u0644\u0623\u062D\u0632\u0627\u0628", ayatCount: 73 },
39
+ { number: 34, name: "Saba", arabic: "\u0633\u0628\u0623", ayatCount: 54 },
40
+ { number: 35, name: "Fatir", arabic: "\u0641\u0627\u0637\u0631", ayatCount: 45 },
41
+ { number: 36, name: "Ya Sin", arabic: "\u064A\u0633", ayatCount: 83 },
42
+ { number: 37, name: "As-Saffat", arabic: "\u0627\u0644\u0635\u0627\u0641\u0627\u062A", ayatCount: 182 },
43
+ { number: 38, name: "Sad", arabic: "\u0635", ayatCount: 88 },
44
+ { number: 39, name: "Az-Zumar", arabic: "\u0627\u0644\u0632\u0645\u0631", ayatCount: 75 },
45
+ { number: 40, name: "Ghafir", arabic: "\u063A\u0627\u0641\u0631", ayatCount: 85 },
46
+ { number: 41, name: "Fussilat", arabic: "\u0641\u0635\u0644\u062A", ayatCount: 54 },
47
+ { number: 42, name: "Ash-Shura", arabic: "\u0627\u0644\u0634\u0648\u0631\u0649", ayatCount: 53 },
48
+ { number: 43, name: "Az-Zukhruf", arabic: "\u0627\u0644\u0632\u062E\u0631\u0641", ayatCount: 89 },
49
+ { number: 44, name: "Ad-Dukhan", arabic: "\u0627\u0644\u062F\u062E\u0627\u0646", ayatCount: 59 },
50
+ { number: 45, name: "Al-Jathiyah", arabic: "\u0627\u0644\u062C\u0627\u062B\u064A\u0629", ayatCount: 37 },
51
+ { number: 46, name: "Al-Ahqaf", arabic: "\u0627\u0644\u0623\u062D\u0642\u0627\u0641", ayatCount: 35 },
52
+ { number: 47, name: "Muhammad", arabic: "\u0645\u062D\u0645\u062F", ayatCount: 38 },
53
+ { number: 48, name: "Al-Fath", arabic: "\u0627\u0644\u0641\u062A\u062D", ayatCount: 29 },
54
+ { number: 49, name: "Al-Hujurat", arabic: "\u0627\u0644\u062D\u062C\u0631\u0627\u062A", ayatCount: 18 },
55
+ { number: 50, name: "Qaf", arabic: "\u0642", ayatCount: 45 },
56
+ { number: 51, name: "Adh-Dhariyat", arabic: "\u0627\u0644\u0630\u0627\u0631\u064A\u0627\u062A", ayatCount: 60 },
57
+ { number: 52, name: "At-Tur", arabic: "\u0627\u0644\u0637\u0648\u0631", ayatCount: 49 },
58
+ { number: 53, name: "An-Najm", arabic: "\u0627\u0644\u0646\u062C\u0645", ayatCount: 62 },
59
+ { number: 54, name: "Al-Qamar", arabic: "\u0627\u0644\u0642\u0645\u0631", ayatCount: 55 },
60
+ { number: 55, name: "Ar-Rahman", arabic: "\u0627\u0644\u0631\u062D\u0645\u0646", ayatCount: 78 },
61
+ { number: 56, name: "Al-Waqi'ah", arabic: "\u0627\u0644\u0648\u0627\u0642\u0639\u0629", ayatCount: 96 },
62
+ { number: 57, name: "Al-Hadid", arabic: "\u0627\u0644\u062D\u062F\u064A\u062F", ayatCount: 29 },
63
+ { number: 58, name: "Al-Mujadilah", arabic: "\u0627\u0644\u0645\u062C\u0627\u062F\u0644\u0629", ayatCount: 22 },
64
+ { number: 59, name: "Al-Hashr", arabic: "\u0627\u0644\u062D\u0634\u0631", ayatCount: 24 },
65
+ { number: 60, name: "Al-Mumtahanah", arabic: "\u0627\u0644\u0645\u0645\u062A\u062D\u0646\u0629", ayatCount: 13 },
66
+ { number: 61, name: "As-Saff", arabic: "\u0627\u0644\u0635\u0641", ayatCount: 14 },
67
+ { number: 62, name: "Al-Jumu'ah", arabic: "\u0627\u0644\u062C\u0645\u0639\u0629", ayatCount: 11 },
68
+ { number: 63, name: "Al-Munafiqun", arabic: "\u0627\u0644\u0645\u0646\u0627\u0641\u0642\u0648\u0646", ayatCount: 11 },
69
+ { number: 64, name: "At-Taghabun", arabic: "\u0627\u0644\u062A\u063A\u0627\u0628\u0646", ayatCount: 18 },
70
+ { number: 65, name: "At-Talaq", arabic: "\u0627\u0644\u0637\u0644\u0627\u0642", ayatCount: 12 },
71
+ { number: 66, name: "At-Tahrim", arabic: "\u0627\u0644\u062A\u062D\u0631\u064A\u0645", ayatCount: 12 },
72
+ { number: 67, name: "Al-Mulk", arabic: "\u0627\u0644\u0645\u0644\u0643", ayatCount: 30 },
73
+ { number: 68, name: "Al-Qalam", arabic: "\u0627\u0644\u0642\u0644\u0645", ayatCount: 52 },
74
+ { number: 69, name: "Al-Haqqah", arabic: "\u0627\u0644\u062D\u0627\u0642\u0629", ayatCount: 52 },
75
+ { number: 70, name: "Al-Ma'arij", arabic: "\u0627\u0644\u0645\u0639\u0627\u0631\u062C", ayatCount: 44 },
76
+ { number: 71, name: "Nuh", arabic: "\u0646\u0648\u062D", ayatCount: 28 },
77
+ { number: 72, name: "Al-Jinn", arabic: "\u0627\u0644\u062C\u0646", ayatCount: 28 },
78
+ { number: 73, name: "Al-Muzzammil", arabic: "\u0627\u0644\u0645\u0632\u0645\u0644", ayatCount: 20 },
79
+ { number: 74, name: "Al-Muddaththir", arabic: "\u0627\u0644\u0645\u062F\u062B\u0631", ayatCount: 56 },
80
+ { number: 75, name: "Al-Qiyamah", arabic: "\u0627\u0644\u0642\u064A\u0627\u0645\u0629", ayatCount: 40 },
81
+ { number: 76, name: "Al-Insan", arabic: "\u0627\u0644\u0625\u0646\u0633\u0627\u0646", ayatCount: 31 },
82
+ { number: 77, name: "Al-Mursalat", arabic: "\u0627\u0644\u0645\u0631\u0633\u0644\u0627\u062A", ayatCount: 50 },
83
+ { number: 78, name: "An-Naba", arabic: "\u0627\u0644\u0646\u0628\u0623", ayatCount: 40 },
84
+ { number: 79, name: "An-Nazi'at", arabic: "\u0627\u0644\u0646\u0627\u0632\u0639\u0627\u062A", ayatCount: 46 },
85
+ { number: 80, name: "Abasa", arabic: "\u0639\u0628\u0633", ayatCount: 42 },
86
+ { number: 81, name: "At-Takwir", arabic: "\u0627\u0644\u062A\u0643\u0648\u064A\u0631", ayatCount: 29 },
87
+ { number: 82, name: "Al-Infitar", arabic: "\u0627\u0644\u0627\u0646\u0641\u0637\u0627\u0631", ayatCount: 19 },
88
+ { number: 83, name: "Al-Mutaffifin", arabic: "\u0627\u0644\u0645\u0637\u0641\u0641\u064A\u0646", ayatCount: 36 },
89
+ { number: 84, name: "Al-Inshiqaq", arabic: "\u0627\u0644\u0627\u0646\u0634\u0642\u0627\u0642", ayatCount: 25 },
90
+ { number: 85, name: "Al-Buruj", arabic: "\u0627\u0644\u0628\u0631\u0648\u062C", ayatCount: 22 },
91
+ { number: 86, name: "At-Tariq", arabic: "\u0627\u0644\u0637\u0627\u0631\u0642", ayatCount: 17 },
92
+ { number: 87, name: "Al-A'la", arabic: "\u0627\u0644\u0623\u0639\u0644\u0649", ayatCount: 19 },
93
+ { number: 88, name: "Al-Ghashiyah", arabic: "\u0627\u0644\u063A\u0627\u0634\u064A\u0629", ayatCount: 26 },
94
+ { number: 89, name: "Al-Fajr", arabic: "\u0627\u0644\u0641\u062C\u0631", ayatCount: 30 },
95
+ { number: 90, name: "Al-Balad", arabic: "\u0627\u0644\u0628\u0644\u062F", ayatCount: 20 },
96
+ { number: 91, name: "Ash-Shams", arabic: "\u0627\u0644\u0634\u0645\u0633", ayatCount: 15 },
97
+ { number: 92, name: "Al-Lail", arabic: "\u0627\u0644\u0644\u064A\u0644", ayatCount: 21 },
98
+ { number: 93, name: "Ad-Dhuha", arabic: "\u0627\u0644\u0636\u062D\u0649", ayatCount: 11 },
99
+ { number: 94, name: "Ash-Sharh", arabic: "\u0627\u0644\u0634\u0631\u062D", ayatCount: 8 },
100
+ { number: 95, name: "At-Tin", arabic: "\u0627\u0644\u062A\u064A\u0646", ayatCount: 8 },
101
+ { number: 96, name: "Al-Alaq", arabic: "\u0627\u0644\u0639\u0644\u0642", ayatCount: 19 },
102
+ { number: 97, name: "Al-Qadr", arabic: "\u0627\u0644\u0642\u062F\u0631", ayatCount: 5 },
103
+ { number: 98, name: "Al-Bayyinah", arabic: "\u0627\u0644\u0628\u064A\u0646\u0629", ayatCount: 8 },
104
+ { number: 99, name: "Az-Zalzalah", arabic: "\u0627\u0644\u0632\u0644\u0632\u0644\u0629", ayatCount: 8 },
105
+ { number: 100, name: "Al-Adiyat", arabic: "\u0627\u0644\u0639\u0627\u062F\u064A\u0627\u062A", ayatCount: 11 },
106
+ { number: 101, name: "Al-Qari'ah", arabic: "\u0627\u0644\u0642\u0627\u0631\u0639\u0629", ayatCount: 11 },
107
+ { number: 102, name: "At-Takathur", arabic: "\u0627\u0644\u062A\u0643\u0627\u062B\u0631", ayatCount: 8 },
108
+ { number: 103, name: "Al-Asr", arabic: "\u0627\u0644\u0639\u0635\u0631", ayatCount: 3 },
109
+ { number: 104, name: "Al-Humazah", arabic: "\u0627\u0644\u0647\u0645\u0632\u0629", ayatCount: 9 },
110
+ { number: 105, name: "Al-Fil", arabic: "\u0627\u0644\u0641\u064A\u0644", ayatCount: 5 },
111
+ { number: 106, name: "Quraysh", arabic: "\u0642\u0631\u064A\u0634", ayatCount: 4 },
112
+ { number: 107, name: "Al-Ma'un", arabic: "\u0627\u0644\u0645\u0627\u0639\u0648\u0646", ayatCount: 7 },
113
+ { number: 108, name: "Al-Kawthar", arabic: "\u0627\u0644\u0643\u0648\u062B\u0631", ayatCount: 3 },
114
+ { number: 109, name: "Al-Kafirun", arabic: "\u0627\u0644\u0643\u0627\u0641\u0631\u0648\u0646", ayatCount: 6 },
115
+ { number: 110, name: "An-Nasr", arabic: "\u0627\u0644\u0646\u0635\u0631", ayatCount: 3 },
116
+ { number: 111, name: "Al-Masad", arabic: "\u0627\u0644\u0645\u0633\u062F", ayatCount: 5 },
117
+ { number: 112, name: "Al-Ikhlas", arabic: "\u0627\u0644\u0625\u062E\u0644\u0627\u0635", ayatCount: 4 },
118
+ { number: 113, name: "Al-Falaq", arabic: "\u0627\u0644\u0641\u0644\u0642", ayatCount: 5 },
119
+ { number: 114, name: "An-Nas", arabic: "\u0627\u0644\u0646\u0627\u0633", ayatCount: 6 }
120
+ ];
121
+ function getSurah(number) {
122
+ return SURAH_LIST.find((s) => s.number === number);
123
+ }
124
+ function getAyatCount(surahNumber) {
125
+ return getSurah(surahNumber)?.ayatCount ?? 0;
126
+ }
127
+
128
+ // src/labels.ts
129
+ var defaultLabels = {
130
+ surah: "Surah",
131
+ ayah: "Ayah",
132
+ subtitle: "Select surah and ayah",
133
+ from: "From",
134
+ to: "To",
135
+ fromSurah: "Start surah",
136
+ fromAyah: "Start ayah",
137
+ toSurah: "End surah",
138
+ toAyah: "End ayah",
139
+ select: "Select",
140
+ cancel: "Cancel"
141
+ };
142
+ function resolveLabels(partial) {
143
+ return { ...defaultLabels, ...partial };
144
+ }
145
+
146
+ // src/core/SurahWheelPicker.tsx
147
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
148
+ function pad2(n) {
149
+ return String(n).padStart(2, "0");
150
+ }
151
+ function vibrate() {
152
+ try {
153
+ if (navigator.vibrate) navigator.vibrate(10);
154
+ } catch {
155
+ }
156
+ }
157
+ var visibleCount = 8;
158
+ var itemHeight = 36;
159
+ var halfItemHeight = itemHeight * 0.5;
160
+ var itemAngle = 360 / visibleCount;
161
+ var radius = itemHeight / Math.tan(itemAngle * Math.PI / 180);
162
+ var containerHeight = Math.round(radius * 2 + itemHeight * 0.25);
163
+ var quarterCount = visibleCount >> 2;
164
+ var RESISTANCE = 0.3;
165
+ var STOP_VELOCITY = 0.5;
166
+ var WHEEL_SETTLE_MS = 90;
167
+ var MIN_FLICK_VELOCITY = 2;
168
+ var VIB_THROTTLE_MS = 30;
169
+ var WHEEL_NOTCH_PX = 100;
170
+ var WHEEL_LINE_PX = 40;
171
+ var easeOutCubic = (p) => Math.pow(p - 1, 3) + 1;
172
+ var clamp = (v, min, max) => Math.max(min, Math.min(v, max));
173
+ function SurahWheelPicker({ value, onChange, nameSurah = "surah_number", nameAyat = "ayat", physics, loop = false, chrome = true, ariaSurahLabel, ariaAyatLabel, labels: labelsProp }) {
174
+ const labels = resolveLabels(labelsProp);
175
+ const surahLabel = ariaSurahLabel ?? labels.surah;
176
+ const ayatLabel = ariaAyatLabel ?? labels.ayah;
177
+ const friction = physics?.friction ?? 5;
178
+ const maxVelocity = physics?.maxVelocity ?? 120;
179
+ const snapMs = physics?.snapMs ?? 120;
180
+ const boundIndex = (norm, len) => liveRef.current.loop ? (norm % len + len) % len : clamp(norm, 0, len - 1);
181
+ const [surah, setSurah] = useState(value?.surah ?? 3);
182
+ const [ayat, setAyat] = useState(value?.ayat ?? 3);
183
+ const surahScrollRef = useRef(0);
184
+ const ayatScrollRef = useRef(0);
185
+ const surahContainerRef = useRef(null);
186
+ const ayatContainerRef = useRef(null);
187
+ const surahWheelRef = useRef(null);
188
+ const ayatWheelRef = useRef(null);
189
+ const surahHighlightRef = useRef(null);
190
+ const ayatHighlightRef = useRef(null);
191
+ const surahMoveId = useRef(0);
192
+ const ayatMoveId = useRef(0);
193
+ const surahDragging = useRef(false);
194
+ const ayatDragging = useRef(false);
195
+ const motionRef = useRef(false);
196
+ const lastVibRef = useRef(0);
197
+ const wheelTimerSurah = useRef(0);
198
+ const wheelTimerAyat = useRef(0);
199
+ const wheelAccumSurah = useRef(0);
200
+ const wheelAccumAyat = useRef(0);
201
+ const wheelTargetSurah = useRef(null);
202
+ const wheelTargetAyat = useRef(null);
203
+ const activeSurah = SURAH_LIST.find((s) => s.number === surah) ?? SURAH_LIST[2];
204
+ const ayatCount = getAyatCount(surah);
205
+ const surahValRef = useRef(surah);
206
+ const ayatValRef = useRef(ayat);
207
+ useEffect(() => {
208
+ surahValRef.current = surah;
209
+ }, [surah]);
210
+ useEffect(() => {
211
+ ayatValRef.current = ayat;
212
+ }, [ayat]);
213
+ const surahOptions = SURAH_LIST;
214
+ const ayatOptions = Array.from({ length: ayatCount }, (_, i) => i + 1);
215
+ const liveRef = useRef({ loop, friction, maxVelocity, snapMs, ayatLen: ayatOptions.length });
216
+ liveRef.current = { loop, friction, maxVelocity, snapMs, ayatLen: ayatOptions.length };
217
+ const onChangeRef = useRef(onChange);
218
+ onChangeRef.current = onChange;
219
+ const ayatLenOf = (isSurah) => isSurah ? surahOptions.length : liveRef.current.ayatLen;
220
+ useEffect(() => {
221
+ if (ayat > ayatCount || ayat < 1) {
222
+ setAyat(1);
223
+ ayatValRef.current = 1;
224
+ onChange({ surah, ayat: 1 });
225
+ }
226
+ }, [surah, ayatCount]);
227
+ useEffect(() => {
228
+ if (!value) return;
229
+ if (value.surah === surahValRef.current && value.ayat === ayatValRef.current) return;
230
+ cancelAll();
231
+ surahValRef.current = value.surah;
232
+ ayatValRef.current = value.ayat;
233
+ setSurah(value.surah);
234
+ setAyat(value.ayat);
235
+ }, [value?.surah, value?.ayat]);
236
+ const scrollTo = useCallback((isSurah, scroll) => {
237
+ const wheelRef = isSurah ? surahWheelRef : ayatWheelRef;
238
+ const highlightRef = isSurah ? surahHighlightRef : ayatHighlightRef;
239
+ const len = ayatLenOf(isSurah);
240
+ if (wheelRef.current) {
241
+ wheelRef.current.childNodes.forEach((node) => {
242
+ const li = node;
243
+ const idx = Number(li.dataset.index);
244
+ let d = (idx - scroll) % len;
245
+ d = (d % len + len) % len;
246
+ if (d > len / 2) d -= len;
247
+ const cache = li;
248
+ if (Math.abs(d) > quarterCount) {
249
+ if (cache._w !== "h") {
250
+ cache._w = "h";
251
+ li.style.visibility = "hidden";
252
+ }
253
+ } else {
254
+ const t = `rotateX(${-itemAngle * d}deg) translateZ(${radius}px)`;
255
+ if (cache._w !== t) {
256
+ cache._w = t;
257
+ li.style.visibility = "visible";
258
+ li.style.transform = t;
259
+ }
260
+ }
261
+ });
262
+ }
263
+ if (highlightRef.current) {
264
+ highlightRef.current.style.transform = `translateY(${-scroll * itemHeight}px)`;
265
+ }
266
+ return scroll;
267
+ }, [surahOptions.length, ayatOptions.length]);
268
+ const cancelSurah = () => cancelAnimationFrame(surahMoveId.current);
269
+ const cancelAyat = () => cancelAnimationFrame(ayatMoveId.current);
270
+ const cancelAll = () => {
271
+ cancelSurah();
272
+ cancelAyat();
273
+ motionRef.current = false;
274
+ };
275
+ const animateScroll = (isSurah, start, end, duration, onComplete) => {
276
+ if (start === end || duration === 0) {
277
+ scrollTo(isSurah, start);
278
+ return;
279
+ }
280
+ motionRef.current = true;
281
+ const startTime = performance.now();
282
+ const dist = end - start;
283
+ const ref = isSurah ? surahScrollRef : ayatScrollRef;
284
+ const moveRef = isSurah ? surahMoveId : ayatMoveId;
285
+ const tick = (now) => {
286
+ const t = performance.now();
287
+ const elapsed = (t - startTime) / 1e3;
288
+ if (elapsed < duration) {
289
+ const p = easeOutCubic(elapsed / duration);
290
+ ref.current = scrollTo(isSurah, start + p * dist);
291
+ moveRef.current = requestAnimationFrame(tick);
292
+ } else {
293
+ motionRef.current = false;
294
+ if (isSurah) cancelSurah();
295
+ else cancelAyat();
296
+ ref.current = scrollTo(isSurah, end);
297
+ onComplete?.();
298
+ }
299
+ };
300
+ requestAnimationFrame(tick);
301
+ };
302
+ const selectByScroll = (isSurah, scroll) => {
303
+ const len = ayatLenOf(isSurah);
304
+ const norm = Math.round(scroll);
305
+ const bounded = boundIndex(norm, len);
306
+ if (isSurah) {
307
+ surahScrollRef.current = scrollTo(true, bounded);
308
+ const sel = surahOptions[surahScrollRef.current];
309
+ if (sel) {
310
+ const n = sel.number;
311
+ if (n !== surahValRef.current) {
312
+ vibrate();
313
+ surahValRef.current = n;
314
+ ayatValRef.current = 1;
315
+ setSurah(n);
316
+ setAyat(1);
317
+ onChangeRef.current({ surah: n, ayat: 1 });
318
+ }
319
+ }
320
+ } else {
321
+ ayatScrollRef.current = scrollTo(false, bounded);
322
+ const n = bounded + 1;
323
+ if (n !== ayatValRef.current) {
324
+ vibrate();
325
+ ayatValRef.current = n;
326
+ setAyat(n);
327
+ onChangeRef.current({ surah: surahValRef.current, ayat: n });
328
+ }
329
+ }
330
+ };
331
+ const throttledVibrate = () => {
332
+ const now = performance.now();
333
+ if (now - lastVibRef.current > VIB_THROTTLE_MS) {
334
+ lastVibRef.current = now;
335
+ vibrate();
336
+ }
337
+ };
338
+ const calcVelocity = (yList) => {
339
+ if (yList.length < 2) return 0;
340
+ const clampV = (v) => clamp(v, -liveRef.current.maxVelocity, liveRef.current.maxVelocity);
341
+ const last = yList[yList.length - 1];
342
+ const pts = yList.filter(([_, t]) => last[1] - t <= 120);
343
+ if (pts.length < 2) {
344
+ const [sy, st] = yList[yList.length - 2];
345
+ const [ey, et] = last;
346
+ const dt = (et - st) / 1e3;
347
+ if (dt <= 0) return 0;
348
+ return clampV((sy - ey) / itemHeight / dt);
349
+ }
350
+ const n = pts.length;
351
+ const t0 = pts[0][1] / 1e3;
352
+ let sx = 0, sy2 = 0, sxx = 0, sxy = 0;
353
+ for (const [y, t] of pts) {
354
+ const x = t / 1e3 - t0;
355
+ sx += x;
356
+ sy2 += y;
357
+ sxx += x * x;
358
+ sxy += x * y;
359
+ }
360
+ const denom = n * sxx - sx * sx;
361
+ if (denom === 0) return 0;
362
+ const slope = (n * sxy - sx * sy2) / denom;
363
+ return clampV(-slope / itemHeight);
364
+ };
365
+ const commitNotch = (isSurah, notch) => {
366
+ const len = ayatLenOf(isSurah);
367
+ const bounded = boundIndex(notch, len);
368
+ if (isSurah) {
369
+ const sel = surahOptions[bounded];
370
+ if (sel && sel.number !== surahValRef.current) {
371
+ surahValRef.current = sel.number;
372
+ ayatValRef.current = 1;
373
+ setSurah(sel.number);
374
+ setAyat(1);
375
+ onChangeRef.current({ surah: sel.number, ayat: 1 });
376
+ }
377
+ } else {
378
+ const n = bounded + 1;
379
+ if (n !== ayatValRef.current) {
380
+ ayatValRef.current = n;
381
+ setAyat(n);
382
+ onChangeRef.current({ surah: surahValRef.current, ayat: n });
383
+ }
384
+ }
385
+ };
386
+ const fling = (isSurah, velocity) => {
387
+ const scrollRef = isSurah ? surahScrollRef : ayatScrollRef;
388
+ const moveRef = isSurah ? surahMoveId : ayatMoveId;
389
+ const len = ayatLenOf(isSurah);
390
+ const { friction: friction2, snapMs: snapMs2, loop: loop2 } = liveRef.current;
391
+ let v = velocity;
392
+ let last = performance.now();
393
+ let lastNotch = Math.round(scrollRef.current);
394
+ motionRef.current = true;
395
+ const step = (_rafNow) => {
396
+ const now = performance.now();
397
+ const dt = clamp((now - last) / 1e3, 0, 0.05);
398
+ last = now;
399
+ v *= Math.exp(-friction2 * dt);
400
+ if (Math.abs(v) < STOP_VELOCITY) {
401
+ const target = loop2 ? Math.round(scrollRef.current) : clamp(Math.round(scrollRef.current), 0, len - 1);
402
+ animateScroll(isSurah, scrollRef.current, target, snapMs2 / 1e3, () => selectByScroll(isSurah, target));
403
+ return;
404
+ }
405
+ let next = scrollRef.current + v * dt;
406
+ if (!loop2 && (next < 0 || next > len - 1)) {
407
+ next = clamp(next, 0, len - 1);
408
+ v = 0;
409
+ }
410
+ scrollRef.current = scrollTo(isSurah, next);
411
+ const notch = Math.round(scrollRef.current);
412
+ if (notch !== lastNotch) {
413
+ lastNotch = notch;
414
+ throttledVibrate();
415
+ commitNotch(isSurah, notch);
416
+ }
417
+ moveRef.current = requestAnimationFrame(step);
418
+ };
419
+ moveRef.current = requestAnimationFrame(step);
420
+ };
421
+ const scrollByStep = (isSurah, step) => {
422
+ const ref = isSurah ? surahScrollRef : ayatScrollRef;
423
+ const start = ref.current;
424
+ const len = ayatLenOf(isSurah);
425
+ let end = Math.round(start + step);
426
+ if (!liveRef.current.loop) end = clamp(end, 0, len - 1);
427
+ const dist = Math.abs(end - start);
428
+ if (dist === 0) return;
429
+ const duration = Math.sqrt(dist / 5);
430
+ cancelAll();
431
+ animateScroll(isSurah, start, end, duration, () => selectByScroll(isSurah, ref.current));
432
+ };
433
+ useEffect(() => {
434
+ const si = surahOptions.findIndex((s) => s.number === surah);
435
+ const ai = ayat - 1;
436
+ surahScrollRef.current = scrollTo(true, si);
437
+ ayatScrollRef.current = scrollTo(false, ai);
438
+ }, []);
439
+ useEffect(() => {
440
+ if (motionRef.current || surahDragging.current) return;
441
+ const si = surahOptions.findIndex((s) => s.number === surah);
442
+ if (si !== -1) {
443
+ surahScrollRef.current = scrollTo(true, si);
444
+ }
445
+ }, [surah, scrollTo, surahOptions]);
446
+ useEffect(() => {
447
+ if (motionRef.current || ayatDragging.current) return;
448
+ ayatScrollRef.current = scrollTo(false, ayat - 1);
449
+ }, [ayat, ayatCount, scrollTo]);
450
+ const makeDrag = (isSurah) => {
451
+ const containerRef = isSurah ? surahContainerRef : ayatContainerRef;
452
+ const draggingRef = isSurah ? surahDragging : ayatDragging;
453
+ const scrollRef = isSurah ? surahScrollRef : ayatScrollRef;
454
+ const wheelTimerRef = isSurah ? wheelTimerSurah : wheelTimerAyat;
455
+ const yList = [];
456
+ let startY = 0;
457
+ let touchScroll = 0;
458
+ let isClick = true;
459
+ let lastTick = 0;
460
+ const onDown = (e) => {
461
+ draggingRef.current = true;
462
+ const y = (e instanceof MouseEvent ? e.clientY : e.touches?.[0]?.clientY) || 0;
463
+ startY = y;
464
+ yList.length = 0;
465
+ yList.push([y, Date.now()]);
466
+ touchScroll = scrollRef.current;
467
+ lastTick = Math.round(touchScroll);
468
+ isClick = true;
469
+ window.clearTimeout(wheelTimerRef.current);
470
+ cancelAll();
471
+ };
472
+ const onMove = (e) => {
473
+ if (!draggingRef.current) return;
474
+ if (e.cancelable) e.preventDefault();
475
+ const y = (e instanceof MouseEvent ? e.clientY : e.touches?.[0]?.clientY) || 0;
476
+ if (isClick && Math.abs(y - startY) > 5) isClick = false;
477
+ yList.push([y, Date.now()]);
478
+ if (yList.length > 5) yList.shift();
479
+ const delta = (startY - y) / itemHeight;
480
+ let next = touchScroll + delta;
481
+ const len = ayatLenOf(isSurah);
482
+ if (!liveRef.current.loop) {
483
+ if (next < 0) next *= RESISTANCE;
484
+ else if (next > len - 1) next = len - 1 + (next - (len - 1)) * RESISTANCE;
485
+ }
486
+ scrollRef.current = scrollTo(isSurah, next);
487
+ const notch = Math.round(scrollRef.current);
488
+ if (notch !== lastTick) {
489
+ lastTick = notch;
490
+ throttledVibrate();
491
+ commitNotch(isSurah, notch);
492
+ }
493
+ };
494
+ const onUp = (e) => {
495
+ if (!draggingRef.current) return;
496
+ draggingRef.current = false;
497
+ if (isClick) {
498
+ const y = (e instanceof MouseEvent ? e.clientY : e.changedTouches?.[0]?.clientY) || 0;
499
+ const container = containerRef.current;
500
+ if (container) {
501
+ const top = container.getBoundingClientRect().top;
502
+ const clickY = y - top;
503
+ const quarter = visibleCount >> 2;
504
+ let pos = 0;
505
+ const degToRad = Math.PI / 180;
506
+ for (let i = quarter - 1; i >= -quarter + 1; --i) {
507
+ const segLen = itemHeight * Math.cos(i * itemAngle * degToRad);
508
+ const start = pos;
509
+ pos += segLen;
510
+ if (clickY >= start && clickY <= pos) {
511
+ const steps = (quarter - i - 1) * -1;
512
+ if (steps !== 0) scrollByStep(isSurah, steps);
513
+ return;
514
+ }
515
+ }
516
+ }
517
+ return;
518
+ }
519
+ cancelAll();
520
+ const live = liveRef.current;
521
+ const len = ayatLenOf(isSurah);
522
+ if (!live.loop && (scrollRef.current < 0 || scrollRef.current > len - 1)) {
523
+ const t = clamp(scrollRef.current, 0, len - 1);
524
+ animateScroll(isSurah, scrollRef.current, t, live.snapMs / 1e3, () => selectByScroll(isSurah, t));
525
+ return;
526
+ }
527
+ const velocity = calcVelocity(yList);
528
+ if (Math.abs(velocity) < MIN_FLICK_VELOCITY) {
529
+ const t = live.loop ? Math.round(scrollRef.current) : clamp(Math.round(scrollRef.current), 0, len - 1);
530
+ animateScroll(isSurah, scrollRef.current, t, live.snapMs / 1e3, () => selectByScroll(isSurah, t));
531
+ return;
532
+ }
533
+ fling(isSurah, velocity);
534
+ };
535
+ return { onDown, onMove, onUp };
536
+ };
537
+ const surahDrag = useRef(null);
538
+ const ayatDrag = useRef(null);
539
+ useEffect(() => {
540
+ const sDrag = makeDrag(true);
541
+ const aDrag = makeDrag(false);
542
+ surahDrag.current = sDrag;
543
+ ayatDrag.current = aDrag;
544
+ const sEl = surahContainerRef.current;
545
+ const aEl = ayatContainerRef.current;
546
+ if (sEl) {
547
+ sEl.addEventListener("touchstart", sDrag.onDown, { passive: false });
548
+ sEl.addEventListener("touchmove", sDrag.onMove, { passive: false });
549
+ sEl.addEventListener("touchend", sDrag.onUp);
550
+ sEl.addEventListener("mousedown", sDrag.onDown);
551
+ document.addEventListener("mousemove", sDrag.onMove);
552
+ document.addEventListener("mouseup", sDrag.onUp);
553
+ }
554
+ if (aEl) {
555
+ aEl.addEventListener("touchstart", aDrag.onDown, { passive: false });
556
+ aEl.addEventListener("touchmove", aDrag.onMove, { passive: false });
557
+ aEl.addEventListener("touchend", aDrag.onUp);
558
+ aEl.addEventListener("mousedown", aDrag.onDown);
559
+ document.addEventListener("mousemove", aDrag.onMove);
560
+ document.addEventListener("mouseup", aDrag.onUp);
561
+ }
562
+ const onWheel = (e, isSurah) => {
563
+ if (e.cancelable) e.preventDefault();
564
+ cancelAll();
565
+ motionRef.current = true;
566
+ const timerRef = isSurah ? wheelTimerSurah : wheelTimerAyat;
567
+ const accRef = isSurah ? wheelAccumSurah : wheelAccumAyat;
568
+ const targetRef = isSurah ? wheelTargetSurah : wheelTargetAyat;
569
+ window.clearTimeout(timerRef.current);
570
+ const ref = isSurah ? surahScrollRef : ayatScrollRef;
571
+ const live = liveRef.current;
572
+ const len = ayatLenOf(isSurah);
573
+ let target = targetRef.current ?? Math.round(ref.current);
574
+ const unit = e.deltaMode === 1 ? WHEEL_LINE_PX : e.deltaMode === 2 ? 400 : 1;
575
+ accRef.current += e.deltaY * unit / WHEEL_NOTCH_PX;
576
+ let guard = 0;
577
+ while (Math.abs(accRef.current) >= 1 && guard++ < 8) {
578
+ const dir = Math.sign(accRef.current);
579
+ const t = target + dir;
580
+ if (!live.loop && (t < 0 || t > len - 1)) {
581
+ accRef.current = 0;
582
+ break;
583
+ }
584
+ accRef.current -= dir;
585
+ target = t;
586
+ throttledVibrate();
587
+ commitNotch(isSurah, t);
588
+ }
589
+ targetRef.current = target;
590
+ animateScroll(isSurah, ref.current, target, live.snapMs / 1e3, void 0);
591
+ timerRef.current = window.setTimeout(() => {
592
+ targetRef.current = null;
593
+ const t = live.loop ? Math.round(ref.current) : clamp(Math.round(ref.current), 0, len - 1);
594
+ animateScroll(isSurah, ref.current, t, live.snapMs / 1e3, () => selectByScroll(isSurah, t));
595
+ }, WHEEL_SETTLE_MS);
596
+ };
597
+ const sWheel = (e) => onWheel(e, true);
598
+ const aWheel = (e) => onWheel(e, false);
599
+ sEl?.addEventListener("wheel", sWheel, { passive: false });
600
+ aEl?.addEventListener("wheel", aWheel, { passive: false });
601
+ return () => {
602
+ window.clearTimeout(wheelTimerSurah.current);
603
+ window.clearTimeout(wheelTimerAyat.current);
604
+ cancelAll();
605
+ sEl?.removeEventListener("touchstart", sDrag.onDown);
606
+ sEl?.removeEventListener("touchmove", sDrag.onMove);
607
+ sEl?.removeEventListener("touchend", sDrag.onUp);
608
+ sEl?.removeEventListener("mousedown", sDrag.onDown);
609
+ document.removeEventListener("mousemove", sDrag.onMove);
610
+ document.removeEventListener("mouseup", sDrag.onUp);
611
+ aEl?.removeEventListener("touchstart", aDrag.onDown);
612
+ aEl?.removeEventListener("touchmove", aDrag.onMove);
613
+ aEl?.removeEventListener("touchend", aDrag.onUp);
614
+ aEl?.removeEventListener("mousedown", aDrag.onDown);
615
+ document.removeEventListener("mousemove", aDrag.onMove);
616
+ document.removeEventListener("mouseup", aDrag.onUp);
617
+ sEl?.removeEventListener("wheel", sWheel);
618
+ aEl?.removeEventListener("wheel", aWheel);
619
+ };
620
+ }, []);
621
+ const onKeySurah = (e) => {
622
+ if (e.key === "ArrowDown") {
623
+ e.preventDefault();
624
+ scrollByStep(true, 1);
625
+ }
626
+ if (e.key === "ArrowUp") {
627
+ e.preventDefault();
628
+ scrollByStep(true, -1);
629
+ }
630
+ };
631
+ const onKeyAyat = (e) => {
632
+ if (e.key === "ArrowDown") {
633
+ e.preventDefault();
634
+ scrollByStep(false, 1);
635
+ }
636
+ if (e.key === "ArrowUp") {
637
+ e.preventDefault();
638
+ scrollByStep(false, -1);
639
+ }
640
+ };
641
+ return /* @__PURE__ */ jsxs("div", { className: chrome ? "rounded-[20px] bg-surface border border-border p-4" : "", children: [
642
+ chrome ? /* @__PURE__ */ jsxs(Fragment, { children: [
643
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-3", children: [
644
+ /* @__PURE__ */ jsxs("div", { children: [
645
+ /* @__PURE__ */ jsx("div", { className: "text-[18px] font-bold leading-none tracking-tight text-foreground", children: labels.surah }),
646
+ /* @__PURE__ */ jsx("div", { className: "text-xs text-muted mt-1", children: labels.subtitle })
647
+ ] }),
648
+ /* @__PURE__ */ jsxs("div", { className: "rounded-xl px-4 py-2.5 text-[13px] font-medium min-w-[170px] text-center border-2 border-accent text-foreground w-full sm:w-auto", children: [
649
+ pad2(surah),
650
+ " ",
651
+ activeSurah.name,
652
+ " : ",
653
+ ayat
654
+ ] })
655
+ ] }),
656
+ /* @__PURE__ */ jsx("input", { type: "hidden", name: nameSurah, value: String(surah) }),
657
+ /* @__PURE__ */ jsx("input", { type: "hidden", name: nameAyat, value: String(ayat) })
658
+ ] }) : null,
659
+ /* @__PURE__ */ jsxs("div", { className: "relative flex gap-2 rounded-2xl bg-accent/5 border border-border p-2 overflow-hidden", style: { perspective: "2000px" }, children: [
660
+ /* @__PURE__ */ jsxs(
661
+ "div",
662
+ {
663
+ ref: surahContainerRef,
664
+ tabIndex: 0,
665
+ role: "listbox",
666
+ "aria-label": surahLabel,
667
+ onKeyDown: onKeySurah,
668
+ className: "flex-1 min-w-0 relative overflow-hidden focus:outline-none focus-visible:ring-2 focus-visible:ring-accent rounded-xl",
669
+ style: { height: containerHeight, maskImage: "linear-gradient(to bottom, transparent 0%, black 20%, black 80%, transparent 100%)" },
670
+ children: [
671
+ /* @__PURE__ */ jsx(
672
+ "ul",
673
+ {
674
+ ref: surahWheelRef,
675
+ className: "absolute top-1/2 left-0 w-full h-0 m-0 p-0 list-none",
676
+ style: { transformStyle: "preserve-3d", willChange: "transform", backfaceVisibility: "hidden" },
677
+ children: SURAH_LIST.map((s, idx) => /* @__PURE__ */ jsx(
678
+ "li",
679
+ {
680
+ "data-index": idx,
681
+ className: "absolute top-0 left-0 w-full flex items-center px-3 cursor-pointer select-none text-[13px]",
682
+ style: {
683
+ top: -halfItemHeight,
684
+ height: itemHeight,
685
+ lineHeight: `${itemHeight}px`,
686
+ visibility: "hidden"
687
+ },
688
+ children: /* @__PURE__ */ jsxs("span", { className: s.number === surah ? "text-foreground font-bold" : "text-muted", children: [
689
+ pad2(s.number),
690
+ " ",
691
+ s.name
692
+ ] })
693
+ },
694
+ s.number
695
+ ))
696
+ }
697
+ ),
698
+ /* @__PURE__ */ jsx("div", { className: "pointer-events-none absolute top-1/2 inset-x-2 -translate-y-1/2 h-9 rounded-xl border-2 border-accent z-10" })
699
+ ]
700
+ }
701
+ ),
702
+ /* @__PURE__ */ jsxs(
703
+ "div",
704
+ {
705
+ ref: ayatContainerRef,
706
+ tabIndex: 0,
707
+ role: "listbox",
708
+ "aria-label": ayatLabel,
709
+ onKeyDown: onKeyAyat,
710
+ className: "flex-1 min-w-0 sm:flex-none sm:w-[88px] relative overflow-hidden shrink-0 border-l border-border focus:outline-none focus-visible:ring-2 focus-visible:ring-accent rounded-xl",
711
+ style: { height: containerHeight, maskImage: "linear-gradient(to bottom, transparent 0%, black 20%, black 80%, transparent 100%)" },
712
+ children: [
713
+ /* @__PURE__ */ jsx(
714
+ "ul",
715
+ {
716
+ ref: ayatWheelRef,
717
+ className: "absolute top-1/2 left-0 w-full h-0 m-0 p-0 list-none",
718
+ style: { transformStyle: "preserve-3d", willChange: "transform", backfaceVisibility: "hidden" },
719
+ children: ayatOptions.map((n, idx) => /* @__PURE__ */ jsx(
720
+ "li",
721
+ {
722
+ "data-index": idx,
723
+ className: "absolute top-0 left-0 w-full flex items-center justify-center cursor-pointer select-none text-[13px]",
724
+ style: {
725
+ top: -halfItemHeight,
726
+ height: itemHeight,
727
+ lineHeight: `${itemHeight}px`,
728
+ visibility: "hidden"
729
+ },
730
+ children: /* @__PURE__ */ jsx("span", { className: n === ayat ? "text-foreground font-bold" : "text-muted", children: n })
731
+ },
732
+ n
733
+ ))
734
+ }
735
+ ),
736
+ /* @__PURE__ */ jsx("div", { className: "pointer-events-none absolute top-1/2 inset-x-2 -translate-y-1/2 h-9 rounded-xl border-2 border-accent z-10" })
737
+ ]
738
+ }
739
+ )
740
+ ] }),
741
+ /* @__PURE__ */ jsxs("div", { className: "sr-only", "aria-hidden": true, children: [
742
+ /* @__PURE__ */ jsx("ul", { ref: surahHighlightRef }),
743
+ /* @__PURE__ */ jsx("ul", { ref: ayatHighlightRef })
744
+ ] })
745
+ ] });
746
+ }
747
+
748
+ // src/core/SurahRangePicker.tsx
749
+ import { useState as useState2 } from "react";
750
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
751
+ var DEFAULT_RANGE = {
752
+ from: { surah: 1, ayat: 1 },
753
+ to: { surah: 2, ayat: 5 }
754
+ };
755
+ var cmpPos = (a, b) => a.surah !== b.surah ? a.surah - b.surah : a.ayat - b.ayat;
756
+ function SurahRangePicker({
757
+ value,
758
+ labels: labelsProp,
759
+ onChange,
760
+ physics,
761
+ loop = false,
762
+ nameFromSurah = "from_surah_number",
763
+ nameFromAyat = "from_ayat",
764
+ nameToSurah = "to_surah_number",
765
+ nameToAyat = "to_ayat"
766
+ }) {
767
+ const labels = resolveLabels(labelsProp);
768
+ const [from, setFrom] = useState2(value?.from ?? DEFAULT_RANGE.from);
769
+ const [to, setTo] = useState2(value?.to ?? DEFAULT_RANGE.to);
770
+ const emit = (next) => {
771
+ setFrom(next.from);
772
+ setTo(next.to);
773
+ onChange(next);
774
+ };
775
+ const handleFrom = (f) => {
776
+ emit({ from: f, to: cmpPos(f, to) > 0 ? f : to });
777
+ };
778
+ const handleTo = (next) => {
779
+ emit({ from: cmpPos(from, next) > 0 ? next : from, to: next });
780
+ };
781
+ const nameOf = (n) => getSurah(n)?.name ?? String(n);
782
+ return /* @__PURE__ */ jsxs2("div", { className: "rounded-[20px] bg-surface border border-border p-4", children: [
783
+ /* @__PURE__ */ jsxs2("div", { className: "flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-3", children: [
784
+ /* @__PURE__ */ jsxs2("div", { children: [
785
+ /* @__PURE__ */ jsx2("div", { className: "text-[18px] font-bold leading-none tracking-tight text-foreground", children: labels.surah }),
786
+ /* @__PURE__ */ jsx2("div", { className: "text-xs text-muted mt-1", children: labels.subtitle })
787
+ ] }),
788
+ /* @__PURE__ */ jsxs2("div", { className: "rounded-xl px-4 py-2.5 text-[13px] font-medium min-w-[170px] text-center border-2 border-accent text-foreground w-full sm:w-auto", children: [
789
+ nameOf(from.surah),
790
+ ":",
791
+ from.ayat,
792
+ " - ",
793
+ nameOf(to.surah),
794
+ ":",
795
+ to.ayat
796
+ ] })
797
+ ] }),
798
+ /* @__PURE__ */ jsx2("input", { type: "hidden", name: nameFromSurah, value: String(from.surah) }),
799
+ /* @__PURE__ */ jsx2("input", { type: "hidden", name: nameFromAyat, value: String(from.ayat) }),
800
+ /* @__PURE__ */ jsx2("input", { type: "hidden", name: nameToSurah, value: String(to.surah) }),
801
+ /* @__PURE__ */ jsx2("input", { type: "hidden", name: nameToAyat, value: String(to.ayat) }),
802
+ /* @__PURE__ */ jsxs2("div", { className: "space-y-4", children: [
803
+ /* @__PURE__ */ jsxs2("div", { className: "space-y-2", children: [
804
+ /* @__PURE__ */ jsx2("h3", { className: "text-sm font-semibold text-foreground", children: labels.from }),
805
+ /* @__PURE__ */ jsx2(
806
+ SurahWheelPicker,
807
+ {
808
+ value: from,
809
+ onChange: handleFrom,
810
+ physics,
811
+ loop,
812
+ chrome: false,
813
+ ariaSurahLabel: labels.fromSurah,
814
+ ariaAyatLabel: labels.fromAyah
815
+ }
816
+ )
817
+ ] }),
818
+ /* @__PURE__ */ jsxs2("div", { className: "space-y-2", children: [
819
+ /* @__PURE__ */ jsx2("h3", { className: "text-sm font-semibold text-foreground", children: labels.to }),
820
+ /* @__PURE__ */ jsx2(
821
+ SurahWheelPicker,
822
+ {
823
+ value: to,
824
+ onChange: handleTo,
825
+ physics,
826
+ loop,
827
+ chrome: false,
828
+ ariaSurahLabel: labels.toSurah,
829
+ ariaAyatLabel: labels.toAyah
830
+ }
831
+ )
832
+ ] })
833
+ ] })
834
+ ] });
835
+ }
836
+
837
+ // src/core/SurahFieldPicker.tsx
838
+ import { useRef as useRef3, useState as useState4 } from "react";
839
+
840
+ // src/ui/Popup.tsx
841
+ import {
842
+ useEffect as useEffect2,
843
+ useLayoutEffect,
844
+ useRef as useRef2,
845
+ useState as useState3
846
+ } from "react";
847
+ import { createPortal } from "react-dom";
848
+ import { jsx as jsx3 } from "react/jsx-runtime";
849
+ var useIsoLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect2;
850
+ var GAP = 8;
851
+ function clamp2(v, min, max) {
852
+ return Math.max(min, Math.min(v, max));
853
+ }
854
+ function place(anchor, popW, popH, placement) {
855
+ const vw = window.innerWidth;
856
+ const vh = window.innerHeight;
857
+ if (placement === "right") {
858
+ let x2 = anchor.right + GAP;
859
+ if (x2 + popW > vw) x2 = anchor.left - popW - GAP;
860
+ const y2 = clamp2(
861
+ anchor.top + anchor.height / 2 - popH / 2,
862
+ GAP,
863
+ Math.max(GAP, vh - popH - GAP)
864
+ );
865
+ return { x: Math.max(GAP, x2), y: y2 };
866
+ }
867
+ let y = anchor.bottom + GAP;
868
+ if (y + popH > vh) y = anchor.top - popH - GAP;
869
+ const x = clamp2(anchor.left, GAP, Math.max(GAP, vw - popW - GAP));
870
+ return { x, y: Math.max(GAP, y) };
871
+ }
872
+ function Popup({
873
+ open,
874
+ onClose,
875
+ anchorRef,
876
+ placement = "bottom",
877
+ label,
878
+ className,
879
+ children
880
+ }) {
881
+ const popRef = useRef2(null);
882
+ const [pos, setPos] = useState3(null);
883
+ useIsoLayoutEffect(() => {
884
+ if (!open) {
885
+ setPos(null);
886
+ return;
887
+ }
888
+ const anchor = anchorRef.current;
889
+ const pop = popRef.current;
890
+ if (!anchor || !pop || typeof window === "undefined") return;
891
+ const doPlace = () => {
892
+ const el = popRef.current;
893
+ const a = anchorRef.current;
894
+ if (!el || !a) return;
895
+ setPos(place(a.getBoundingClientRect(), el.offsetWidth, el.offsetHeight, placement));
896
+ };
897
+ doPlace();
898
+ window.addEventListener("resize", doPlace);
899
+ window.addEventListener("scroll", doPlace, true);
900
+ return () => {
901
+ window.removeEventListener("resize", doPlace);
902
+ window.removeEventListener("scroll", doPlace, true);
903
+ };
904
+ }, [open, anchorRef, placement, children]);
905
+ useEffect2(() => {
906
+ if (!open || typeof document === "undefined") return;
907
+ const onKey = (e) => {
908
+ if (e.key === "Escape") onClose();
909
+ };
910
+ const onPointer = (e) => {
911
+ const t = e.target;
912
+ if (popRef.current?.contains(t)) return;
913
+ if (anchorRef.current?.contains(t)) return;
914
+ onClose();
915
+ };
916
+ document.addEventListener("keydown", onKey);
917
+ document.addEventListener("pointerdown", onPointer);
918
+ return () => {
919
+ document.removeEventListener("keydown", onKey);
920
+ document.removeEventListener("pointerdown", onPointer);
921
+ };
922
+ }, [open, onClose, anchorRef]);
923
+ useEffect2(() => {
924
+ if (!open && typeof document !== "undefined") {
925
+ const active = document.activeElement;
926
+ if (active && popRef.current?.contains(active)) {
927
+ anchorRef.current?.focus();
928
+ }
929
+ }
930
+ }, [open, anchorRef]);
931
+ if (!open || typeof document === "undefined") return null;
932
+ return createPortal(
933
+ /* @__PURE__ */ jsx3(
934
+ "div",
935
+ {
936
+ ref: popRef,
937
+ role: "dialog",
938
+ "aria-label": label,
939
+ "aria-modal": "false",
940
+ className: `sp-popup ${className ?? ""}`,
941
+ style: pos ? { left: pos.x, top: pos.y, visibility: "visible" } : { left: 0, top: 0, visibility: "hidden" },
942
+ children
943
+ }
944
+ ),
945
+ document.body
946
+ );
947
+ }
948
+
949
+ // src/icons/icons.tsx
950
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
951
+ function ChevronDownIcon({ className }) {
952
+ return /* @__PURE__ */ jsx4(
953
+ "svg",
954
+ {
955
+ className,
956
+ width: "16",
957
+ height: "16",
958
+ viewBox: "0 0 24 24",
959
+ fill: "none",
960
+ stroke: "currentColor",
961
+ strokeWidth: "2",
962
+ strokeLinecap: "round",
963
+ strokeLinejoin: "round",
964
+ "aria-hidden": "true",
965
+ children: /* @__PURE__ */ jsx4("polyline", { points: "6 9 12 15 18 9" })
966
+ }
967
+ );
968
+ }
969
+ function BookIcon({ className }) {
970
+ return /* @__PURE__ */ jsxs3(
971
+ "svg",
972
+ {
973
+ className,
974
+ width: "16",
975
+ height: "16",
976
+ viewBox: "0 0 24 24",
977
+ fill: "none",
978
+ stroke: "currentColor",
979
+ strokeWidth: "2",
980
+ strokeLinecap: "round",
981
+ strokeLinejoin: "round",
982
+ "aria-hidden": "true",
983
+ children: [
984
+ /* @__PURE__ */ jsx4("path", { d: "M4 19.5A2.5 2.5 0 0 1 6.5 17H20" }),
985
+ /* @__PURE__ */ jsx4("path", { d: "M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" })
986
+ ]
987
+ }
988
+ );
989
+ }
990
+
991
+ // src/core/SurahFieldPicker.tsx
992
+ import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
993
+ function SurahFieldPicker({
994
+ value,
995
+ onChange,
996
+ physics,
997
+ loop = false,
998
+ placeholder,
999
+ className,
1000
+ popoverClassName,
1001
+ icon,
1002
+ placement = "right",
1003
+ labels: labelsProp,
1004
+ nameSurah = "surah_number",
1005
+ nameAyat = "ayat"
1006
+ }) {
1007
+ const labels = resolveLabels(labelsProp);
1008
+ const [committed, setCommitted] = useState4(value ?? { surah: 3, ayat: 3 });
1009
+ const [draft, setDraft] = useState4(committed);
1010
+ const [open, setOpen] = useState4(false);
1011
+ const fieldRef = useRef3(null);
1012
+ const current = value ?? committed;
1013
+ const text = placeholder && !value ? placeholder : `${getSurah(current.surah)?.name ?? current.surah}:${current.ayat}`;
1014
+ const handleOpenChange = (next) => {
1015
+ if (next) setDraft(current);
1016
+ setOpen(next);
1017
+ };
1018
+ const handleSelect = () => {
1019
+ setCommitted(draft);
1020
+ onChange(draft);
1021
+ setOpen(false);
1022
+ };
1023
+ return /* @__PURE__ */ jsxs4(Fragment2, { children: [
1024
+ /* @__PURE__ */ jsx5("input", { type: "hidden", name: nameSurah, value: String(current.surah) }),
1025
+ /* @__PURE__ */ jsx5("input", { type: "hidden", name: nameAyat, value: String(current.ayat) }),
1026
+ /* @__PURE__ */ jsxs4(
1027
+ "button",
1028
+ {
1029
+ ref: fieldRef,
1030
+ type: "button",
1031
+ onClick: () => handleOpenChange(!open),
1032
+ className: `flex w-full items-center justify-between gap-2 rounded-xl border border-divider bg-background px-3 py-2.5 text-sm text-foreground ${className ?? ""}`,
1033
+ children: [
1034
+ /* @__PURE__ */ jsx5("span", { className: "truncate", children: text }),
1035
+ icon ?? /* @__PURE__ */ jsx5(BookIcon, { className: "w-4 h-4 shrink-0 text-muted" })
1036
+ ]
1037
+ }
1038
+ ),
1039
+ /* @__PURE__ */ jsx5(
1040
+ Popup,
1041
+ {
1042
+ open,
1043
+ onClose: () => setOpen(false),
1044
+ anchorRef: fieldRef,
1045
+ placement,
1046
+ label: labels.surah,
1047
+ className: popoverClassName,
1048
+ children: /* @__PURE__ */ jsxs4("div", { className: "w-[320px] max-w-[calc(100vw-2rem)] p-3", children: [
1049
+ /* @__PURE__ */ jsx5(
1050
+ SurahWheelPicker,
1051
+ {
1052
+ value: draft,
1053
+ onChange: setDraft,
1054
+ physics,
1055
+ loop,
1056
+ chrome: false,
1057
+ labels: labelsProp,
1058
+ nameSurah,
1059
+ nameAyat
1060
+ }
1061
+ ),
1062
+ /* @__PURE__ */ jsxs4("div", { className: "mt-3 flex justify-end gap-2", children: [
1063
+ /* @__PURE__ */ jsx5("button", { type: "button", className: "sp-btn sp-btn-ghost", onClick: () => setOpen(false), children: labels.cancel }),
1064
+ /* @__PURE__ */ jsx5("button", { type: "button", className: "sp-btn sp-btn-primary", onClick: handleSelect, children: labels.select })
1065
+ ] })
1066
+ ] })
1067
+ }
1068
+ )
1069
+ ] });
1070
+ }
1071
+ export {
1072
+ BookIcon,
1073
+ ChevronDownIcon,
1074
+ Popup,
1075
+ SURAH_LIST,
1076
+ SurahFieldPicker,
1077
+ SurahRangePicker,
1078
+ SurahWheelPicker,
1079
+ defaultLabels,
1080
+ getAyatCount,
1081
+ getSurah,
1082
+ resolveLabels
1083
+ };
1084
+ //# sourceMappingURL=index.js.map