tekiyo-physics 1.0.0 → 1.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 CHANGED
@@ -1,8 +1,8 @@
1
1
  var __defProp = Object.defineProperty;
2
2
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
- import { createContext, useContext, useMemo, useRef, useState, useEffect, useCallback, forwardRef } from "react";
5
- import { jsx } from "react/jsx-runtime";
4
+ import React, { createContext, useContext, useMemo, useRef, useState, useEffect, useCallback, forwardRef } from "react";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
6
  class Vector2 {
7
7
  constructor(x = 0, y = 0) {
8
8
  this.x = x;
@@ -1873,39 +1873,2342 @@ const cardBaseStyles = {
1873
1873
  background: "white",
1874
1874
  boxShadow: "0 4px 12px rgba(0, 0, 0, 0.08)"
1875
1875
  };
1876
+ function PhysicsSlider({
1877
+ label,
1878
+ description,
1879
+ value,
1880
+ min,
1881
+ max,
1882
+ step,
1883
+ config,
1884
+ onChange,
1885
+ decimals = 3,
1886
+ forceDrag = false,
1887
+ className = ""
1888
+ }) {
1889
+ const trackRef = useRef(null);
1890
+ const thumbRef = useRef(null);
1891
+ const state = useRef({
1892
+ isDragging: false,
1893
+ displayPercent: (value - min) / (max - min) * 100,
1894
+ thumbScale: 1,
1895
+ verticalOffset: 0,
1896
+ stretchX: 1,
1897
+ stretchY: 1,
1898
+ stretchOriginPercent: 50,
1899
+ targetOriginPercent: 50,
1900
+ shadowY: 2,
1901
+ shadowBlur: 6,
1902
+ shadowOpacity: 0.12,
1903
+ lastPercent: (value - min) / (max - min) * 100,
1904
+ currentVelocity: 0,
1905
+ pointerStart: null
1906
+ });
1907
+ const [, forceRender] = React.useState(0);
1908
+ const rerender = useCallback(() => forceRender((n) => n + 1), []);
1909
+ const springRef = useRef(null);
1910
+ const momentumRef = useRef(null);
1911
+ const scaleSpringRef = useRef(null);
1912
+ const verticalSpringRef = useRef(null);
1913
+ const stretchXSpringRef = useRef(null);
1914
+ const stretchYSpringRef = useRef(null);
1915
+ const stretchOriginSpringRef = useRef(null);
1916
+ const shadowSpringRef = useRef(null);
1917
+ const velocityTracker = useRef(new VelocityTracker1D());
1918
+ const physicsIdRef = useRef(generatePhysicsId("slider"));
1919
+ const scaleIdRef = useRef(generatePhysicsId("scale"));
1920
+ const verticalIdRef = useRef(generatePhysicsId("sliderVertical"));
1921
+ const stretchXIdRef = useRef(generatePhysicsId("stretchX"));
1922
+ const stretchYIdRef = useRef(generatePhysicsId("stretchY"));
1923
+ const stretchOriginIdRef = useRef(generatePhysicsId("stretchOrigin"));
1924
+ const shadowIdRef = useRef(generatePhysicsId("shadow"));
1925
+ const dragDecayIdRef = useRef(generatePhysicsId("dragDecay"));
1926
+ useEffect(() => {
1927
+ if (!state.current.isDragging && !forceDrag) {
1928
+ const targetPercent = (value - min) / (max - min) * 100;
1929
+ animateToPercent(targetPercent);
1930
+ }
1931
+ }, [value, min, max]);
1932
+ const lastValueRef = useRef(value);
1933
+ useEffect(() => {
1934
+ if (!forceDrag) {
1935
+ lastValueRef.current = value;
1936
+ return;
1937
+ }
1938
+ const s2 = state.current;
1939
+ const targetPercent = (value - min) / (max - min) * 100;
1940
+ const lastPercent = (lastValueRef.current - min) / (max - min) * 100;
1941
+ const velocity = targetPercent - lastPercent;
1942
+ lastValueRef.current = value;
1943
+ s2.thumbScale = config.liftScale;
1944
+ const absVelocity = Math.abs(velocity);
1945
+ const velStretch = Math.min(absVelocity * config.velocityStretch * 15, 1.2);
1946
+ s2.stretchX = 1 + velStretch;
1947
+ s2.stretchY = 1 - velStretch * 0.4;
1948
+ if (Math.abs(velocity) > 0.1) {
1949
+ s2.stretchOriginPercent = velocity > 0 ? 100 : 0;
1950
+ }
1951
+ s2.shadowY = 2 + absVelocity * 0.4;
1952
+ s2.shadowBlur = 6 + absVelocity * 0.6;
1953
+ s2.shadowOpacity = 0.12 + absVelocity * 0.015;
1954
+ s2.displayPercent = targetPercent;
1955
+ s2.isDragging = true;
1956
+ rerender();
1957
+ }, [forceDrag, value, min, max, config.liftScale, config.velocityStretch, rerender]);
1958
+ const animateToPercent = useCallback((targetPercent) => {
1959
+ const s2 = state.current;
1960
+ PhysicsEngine.unregister(physicsIdRef.current);
1961
+ springRef.current = new Spring1D(s2.displayPercent, {
1962
+ tension: config.springTension * 400,
1963
+ friction: 26,
1964
+ mass: 1,
1965
+ precision: 0.01
1966
+ });
1967
+ springRef.current.setTarget(targetPercent);
1968
+ PhysicsEngine.register(physicsIdRef.current, (dt) => {
1969
+ if (!springRef.current) return;
1970
+ const result = springRef.current.step(dt);
1971
+ s2.displayPercent = result.value;
1972
+ rerender();
1973
+ if (result.isSettled) {
1974
+ PhysicsEngine.unregister(physicsIdRef.current);
1975
+ }
1976
+ });
1977
+ }, [config.springTension, rerender]);
1978
+ const percentToValue = useCallback((percent) => {
1979
+ const raw = min + percent / 100 * (max - min);
1980
+ return Math.round(raw / step) * step;
1981
+ }, [min, max, step]);
1982
+ const animateScaleTo = useCallback((targetScale) => {
1983
+ const s2 = state.current;
1984
+ PhysicsEngine.unregister(scaleIdRef.current);
1985
+ scaleSpringRef.current = new Spring1D(s2.thumbScale, {
1986
+ tension: 400,
1987
+ friction: 20,
1988
+ mass: 0.6,
1989
+ precision: 1e-3
1990
+ });
1991
+ scaleSpringRef.current.setTarget(targetScale);
1992
+ PhysicsEngine.register(scaleIdRef.current, (dt) => {
1993
+ if (!scaleSpringRef.current) return;
1994
+ const result = scaleSpringRef.current.step(dt);
1995
+ s2.thumbScale = result.value;
1996
+ rerender();
1997
+ if (result.isSettled) {
1998
+ PhysicsEngine.unregister(scaleIdRef.current);
1999
+ }
2000
+ });
2001
+ }, [rerender]);
2002
+ const getMomentumDecay = useCallback((flickMs) => {
2003
+ const clamped = Math.max(100, Math.min(2e3, flickMs));
2004
+ const seconds = clamped / 1e3;
2005
+ const targetRatio = 0.05;
2006
+ return Math.exp(Math.log(targetRatio) / (seconds * 60));
2007
+ }, []);
2008
+ const animateStretchRelax = useCallback(() => {
2009
+ const s2 = state.current;
2010
+ PhysicsEngine.unregister(stretchXIdRef.current);
2011
+ stretchXSpringRef.current = new Spring1D(s2.stretchX, {
2012
+ tension: 300,
2013
+ friction: 14,
2014
+ mass: 0.5,
2015
+ precision: 1e-3
2016
+ });
2017
+ stretchXSpringRef.current.setTarget(1);
2018
+ PhysicsEngine.register(stretchXIdRef.current, (dt) => {
2019
+ if (!stretchXSpringRef.current) return;
2020
+ const result = stretchXSpringRef.current.step(dt);
2021
+ s2.stretchX = result.value;
2022
+ rerender();
2023
+ if (result.isSettled) {
2024
+ s2.stretchX = 1;
2025
+ PhysicsEngine.unregister(stretchXIdRef.current);
2026
+ }
2027
+ });
2028
+ PhysicsEngine.unregister(stretchYIdRef.current);
2029
+ stretchYSpringRef.current = new Spring1D(s2.stretchY, {
2030
+ tension: 300,
2031
+ friction: 14,
2032
+ mass: 0.5,
2033
+ precision: 1e-3
2034
+ });
2035
+ stretchYSpringRef.current.setTarget(1);
2036
+ PhysicsEngine.register(stretchYIdRef.current, (dt) => {
2037
+ if (!stretchYSpringRef.current) return;
2038
+ const result = stretchYSpringRef.current.step(dt);
2039
+ s2.stretchY = result.value;
2040
+ rerender();
2041
+ if (result.isSettled) {
2042
+ s2.stretchY = 1;
2043
+ PhysicsEngine.unregister(stretchYIdRef.current);
2044
+ }
2045
+ });
2046
+ PhysicsEngine.unregister(shadowIdRef.current);
2047
+ shadowSpringRef.current = new Spring1D(s2.shadowY, {
2048
+ tension: 250,
2049
+ friction: 20,
2050
+ mass: 0.5,
2051
+ precision: 0.01
2052
+ });
2053
+ shadowSpringRef.current.setTarget(2);
2054
+ PhysicsEngine.register(shadowIdRef.current, (dt) => {
2055
+ if (!shadowSpringRef.current) return;
2056
+ const result = shadowSpringRef.current.step(dt);
2057
+ const t = (result.value - 2) / 10;
2058
+ s2.shadowY = result.value;
2059
+ s2.shadowBlur = 6 + t * 4;
2060
+ s2.shadowOpacity = 0.12 + t * 0.03;
2061
+ rerender();
2062
+ if (result.isSettled) {
2063
+ s2.shadowY = 2;
2064
+ s2.shadowBlur = 6;
2065
+ s2.shadowOpacity = 0.12;
2066
+ PhysicsEngine.unregister(shadowIdRef.current);
2067
+ }
2068
+ });
2069
+ PhysicsEngine.unregister(stretchOriginIdRef.current);
2070
+ stretchOriginSpringRef.current = new Spring1D(s2.stretchOriginPercent, {
2071
+ tension: 300,
2072
+ friction: 18,
2073
+ mass: 0.5,
2074
+ precision: 0.1
2075
+ });
2076
+ stretchOriginSpringRef.current.setTarget(50);
2077
+ PhysicsEngine.register(stretchOriginIdRef.current, (dt) => {
2078
+ if (!stretchOriginSpringRef.current) return;
2079
+ const result = stretchOriginSpringRef.current.step(dt);
2080
+ s2.stretchOriginPercent = result.value;
2081
+ s2.targetOriginPercent = 50;
2082
+ rerender();
2083
+ if (result.isSettled) {
2084
+ s2.stretchOriginPercent = 50;
2085
+ s2.targetOriginPercent = 50;
2086
+ PhysicsEngine.unregister(stretchOriginIdRef.current);
2087
+ }
2088
+ });
2089
+ }, [rerender]);
2090
+ const handlePointerDown = useCallback((e) => {
2091
+ if (!trackRef.current) return;
2092
+ const s2 = state.current;
2093
+ e.preventDefault();
2094
+ e.target.setPointerCapture(e.pointerId);
2095
+ s2.pointerStart = { x: e.clientX, y: e.clientY };
2096
+ s2.isDragging = true;
2097
+ PhysicsEngine.unregister(physicsIdRef.current);
2098
+ PhysicsEngine.unregister(verticalIdRef.current);
2099
+ PhysicsEngine.unregister(stretchXIdRef.current);
2100
+ PhysicsEngine.unregister(stretchYIdRef.current);
2101
+ PhysicsEngine.unregister(stretchOriginIdRef.current);
2102
+ PhysicsEngine.unregister(shadowIdRef.current);
2103
+ velocityTracker.current.reset();
2104
+ animateScaleTo(config.liftScale);
2105
+ const rect = trackRef.current.getBoundingClientRect();
2106
+ const x = Math.max(0, Math.min(rect.width, e.clientX - rect.left));
2107
+ const percent = x / rect.width * 100;
2108
+ s2.displayPercent = percent;
2109
+ s2.lastPercent = percent;
2110
+ velocityTracker.current.addSample(percent);
2111
+ onChange(percentToValue(percent));
2112
+ PhysicsEngine.register(dragDecayIdRef.current, () => {
2113
+ if (!s2.isDragging) return;
2114
+ const decayLerp = 0.08;
2115
+ const threshold = 1e-3;
2116
+ if (Math.abs(s2.stretchX - 1) > threshold) {
2117
+ s2.stretchX += (1 - s2.stretchX) * decayLerp;
2118
+ }
2119
+ if (Math.abs(s2.stretchY - 1) > threshold) {
2120
+ s2.stretchY += (1 - s2.stretchY) * decayLerp;
2121
+ }
2122
+ if (Math.abs(s2.stretchOriginPercent - 50) > threshold) {
2123
+ s2.stretchOriginPercent += (50 - s2.stretchOriginPercent) * decayLerp;
2124
+ }
2125
+ s2.shadowY += (2 - s2.shadowY) * decayLerp;
2126
+ s2.shadowBlur += (6 - s2.shadowBlur) * decayLerp;
2127
+ s2.shadowOpacity += (0.12 - s2.shadowOpacity) * decayLerp;
2128
+ rerender();
2129
+ });
2130
+ rerender();
2131
+ }, [onChange, percentToValue, animateScaleTo, config.liftScale, rerender]);
2132
+ const handlePointerMove = useCallback((e) => {
2133
+ const s2 = state.current;
2134
+ if (!s2.isDragging || !trackRef.current || !s2.pointerStart) return;
2135
+ const rect = trackRef.current.getBoundingClientRect();
2136
+ const rawX = e.clientX - rect.left;
2137
+ const rawPercent = rawX / rect.width * 100;
2138
+ let percent = rawPercent;
2139
+ let edgeStretchH = 0;
2140
+ if (rawPercent < 0) {
2141
+ edgeStretchH = -rawPercent / 100;
2142
+ percent = rawPercent * 0.15;
2143
+ s2.targetOriginPercent = 100;
2144
+ } else if (rawPercent > 100) {
2145
+ edgeStretchH = (rawPercent - 100) / 100;
2146
+ percent = 100 + (rawPercent - 100) * 0.15;
2147
+ s2.targetOriginPercent = 0;
2148
+ }
2149
+ const velocity = percent - s2.lastPercent;
2150
+ s2.lastPercent = percent;
2151
+ s2.currentVelocity = velocity;
2152
+ if (Math.abs(velocity) > 0.3 && edgeStretchH === 0) {
2153
+ s2.targetOriginPercent = velocity > 0 ? 100 : 0;
2154
+ }
2155
+ const originLerpSpeed = 0.15;
2156
+ s2.stretchOriginPercent += (s2.targetOriginPercent - s2.stretchOriginPercent) * originLerpSpeed;
2157
+ const rawVerticalOffset = e.clientY - s2.pointerStart.y;
2158
+ const rubberBandedOffset = rawVerticalOffset * 0.06;
2159
+ s2.verticalOffset = Math.max(-5, Math.min(5, rubberBandedOffset));
2160
+ const absVelocity = Math.abs(velocity);
2161
+ const velStretch = Math.min(absVelocity * config.velocityStretch * 15, 1.2);
2162
+ const edgeStretchFactorH = Math.min(Math.abs(edgeStretchH) * 0.6, 0.12);
2163
+ const edgeStretchFactorV = Math.abs(s2.verticalOffset) / 5 * 0.06;
2164
+ const targetStretchX = 1 + velStretch + edgeStretchFactorH;
2165
+ const targetStretchY = 1 - velStretch * 0.4 + edgeStretchFactorV;
2166
+ const baseLerp = 0.35;
2167
+ const decayLerp = 0.15;
2168
+ const velocityThreshold = 0.5;
2169
+ if (absVelocity < velocityThreshold) {
2170
+ const decayFactor = 1 - absVelocity / velocityThreshold;
2171
+ s2.stretchX += (1 - s2.stretchX) * decayLerp * decayFactor;
2172
+ s2.stretchY += (1 - s2.stretchY) * decayLerp * decayFactor;
2173
+ s2.stretchOriginPercent += (50 - s2.stretchOriginPercent) * decayLerp * decayFactor;
2174
+ }
2175
+ s2.stretchX += (targetStretchX - s2.stretchX) * baseLerp;
2176
+ s2.stretchY += (targetStretchY - s2.stretchY) * baseLerp;
2177
+ const targetShadowY = 2 + absVelocity * 0.4;
2178
+ const targetShadowBlur = 6 + absVelocity * 0.6;
2179
+ const targetShadowOpacity = 0.12 + absVelocity * 0.015;
2180
+ const shadowLerp = 0.3;
2181
+ s2.shadowY += (targetShadowY - s2.shadowY) * shadowLerp;
2182
+ s2.shadowBlur += (targetShadowBlur - s2.shadowBlur) * shadowLerp;
2183
+ s2.shadowOpacity += (targetShadowOpacity - s2.shadowOpacity) * shadowLerp;
2184
+ s2.displayPercent = Math.max(0, Math.min(100, percent));
2185
+ velocityTracker.current.addSample(percent);
2186
+ onChange(percentToValue(Math.max(0, Math.min(100, percent))));
2187
+ rerender();
2188
+ }, [onChange, percentToValue, config.velocityStretch, rerender]);
2189
+ const handlePointerUp = useCallback(() => {
2190
+ const s2 = state.current;
2191
+ if (!s2.isDragging) return;
2192
+ s2.isDragging = false;
2193
+ s2.pointerStart = null;
2194
+ PhysicsEngine.unregister(dragDecayIdRef.current);
2195
+ animateScaleTo(1);
2196
+ animateStretchRelax();
2197
+ if (s2.verticalOffset !== 0) {
2198
+ PhysicsEngine.unregister(verticalIdRef.current);
2199
+ verticalSpringRef.current = new Spring1D(s2.verticalOffset, {
2200
+ tension: 400,
2201
+ friction: 18,
2202
+ mass: 0.5,
2203
+ precision: 0.1
2204
+ });
2205
+ verticalSpringRef.current.setTarget(0);
2206
+ PhysicsEngine.register(verticalIdRef.current, (dt) => {
2207
+ if (!verticalSpringRef.current) return;
2208
+ const result = verticalSpringRef.current.step(dt);
2209
+ s2.verticalOffset = result.value;
2210
+ rerender();
2211
+ if (result.isSettled) {
2212
+ s2.verticalOffset = 0;
2213
+ PhysicsEngine.unregister(verticalIdRef.current);
2214
+ }
2215
+ });
2216
+ }
2217
+ const velocity = velocityTracker.current.getVelocity();
2218
+ velocityTracker.current.reset();
2219
+ if (Math.abs(velocity) > 30) {
2220
+ const decay = getMomentumDecay(config.flickMomentum);
2221
+ momentumRef.current = new Momentum1D(s2.displayPercent, {
2222
+ decay,
2223
+ stopThreshold: 0.3,
2224
+ maxVelocity: config.maxVelocity * 100
2225
+ });
2226
+ momentumRef.current.start(s2.displayPercent, velocity * 0.4);
2227
+ PhysicsEngine.register(physicsIdRef.current, (dt) => {
2228
+ if (!momentumRef.current) return;
2229
+ const result = momentumRef.current.step(dt);
2230
+ const momentumVel = Math.abs(result.velocity || 0) / 100;
2231
+ if (momentumVel > 0.1) {
2232
+ const stretch = Math.min(momentumVel * config.velocityStretch * 5, 0.8);
2233
+ s2.stretchX = 1 + stretch;
2234
+ s2.stretchY = 1 - stretch * 0.5;
2235
+ s2.targetOriginPercent = (result.velocity || 0) > 0 ? 100 : 0;
2236
+ s2.stretchOriginPercent += (s2.targetOriginPercent - s2.stretchOriginPercent) * 0.1;
2237
+ }
2238
+ let newPercent = Math.max(0, Math.min(100, result.position));
2239
+ if (result.position < 0 || result.position > 100) {
2240
+ momentumRef.current.stop();
2241
+ const targetPercent = result.position < 0 ? 0 : 100;
2242
+ animateStretchRelax();
2243
+ springRef.current = new Spring1D(newPercent, {
2244
+ tension: config.springTension * 400,
2245
+ friction: 26
2246
+ });
2247
+ springRef.current.setTarget(targetPercent);
2248
+ PhysicsEngine.register(physicsIdRef.current, (dt2) => {
2249
+ if (!springRef.current) return;
2250
+ const springResult = springRef.current.step(dt2);
2251
+ s2.displayPercent = springResult.value;
2252
+ onChange(percentToValue(springResult.value));
2253
+ rerender();
2254
+ if (springResult.isSettled) {
2255
+ PhysicsEngine.unregister(physicsIdRef.current);
2256
+ }
2257
+ });
2258
+ return;
2259
+ }
2260
+ s2.displayPercent = newPercent;
2261
+ onChange(percentToValue(newPercent));
2262
+ rerender();
2263
+ if (!result.isActive) {
2264
+ animateStretchRelax();
2265
+ PhysicsEngine.unregister(physicsIdRef.current);
2266
+ }
2267
+ });
2268
+ }
2269
+ rerender();
2270
+ }, [config, onChange, percentToValue, animateScaleTo, animateStretchRelax, getMomentumDecay, rerender]);
2271
+ const handleTrackClick = useCallback((e) => {
2272
+ const s2 = state.current;
2273
+ if (!trackRef.current || s2.isDragging) return;
2274
+ const rect = trackRef.current.getBoundingClientRect();
2275
+ const x = e.clientX - rect.left;
2276
+ const percent = Math.max(0, Math.min(100, x / rect.width * 100));
2277
+ onChange(percentToValue(percent));
2278
+ animateToPercent(percent);
2279
+ }, [onChange, percentToValue, animateToPercent]);
2280
+ useEffect(() => {
2281
+ return () => {
2282
+ PhysicsEngine.unregister(physicsIdRef.current);
2283
+ PhysicsEngine.unregister(scaleIdRef.current);
2284
+ PhysicsEngine.unregister(verticalIdRef.current);
2285
+ PhysicsEngine.unregister(stretchXIdRef.current);
2286
+ PhysicsEngine.unregister(stretchYIdRef.current);
2287
+ PhysicsEngine.unregister(stretchOriginIdRef.current);
2288
+ PhysicsEngine.unregister(shadowIdRef.current);
2289
+ PhysicsEngine.unregister(dragDecayIdRef.current);
2290
+ };
2291
+ }, []);
2292
+ const s = state.current;
2293
+ return /* @__PURE__ */ jsxs("div", { className: `tekiyo-slider-container ${className}`, children: [
2294
+ /* @__PURE__ */ jsxs("div", { className: "tekiyo-slider-header", children: [
2295
+ /* @__PURE__ */ jsxs("div", { className: "tekiyo-slider-label-group", children: [
2296
+ /* @__PURE__ */ jsx("span", { className: "tekiyo-slider-label", children: label }),
2297
+ description && /* @__PURE__ */ jsx("span", { className: "tekiyo-slider-description", children: description })
2298
+ ] }),
2299
+ /* @__PURE__ */ jsx("span", { className: "tekiyo-slider-value", children: value.toFixed(decimals) })
2300
+ ] }),
2301
+ /* @__PURE__ */ jsxs(
2302
+ "div",
2303
+ {
2304
+ className: "tekiyo-slider-track",
2305
+ ref: trackRef,
2306
+ onClick: handleTrackClick,
2307
+ children: [
2308
+ /* @__PURE__ */ jsx(
2309
+ "div",
2310
+ {
2311
+ className: "tekiyo-slider-fill",
2312
+ style: { width: `${s.displayPercent}%` }
2313
+ }
2314
+ ),
2315
+ /* @__PURE__ */ jsx(
2316
+ "div",
2317
+ {
2318
+ ref: thumbRef,
2319
+ className: `tekiyo-slider-thumb ${s.isDragging ? "active" : ""}`,
2320
+ style: {
2321
+ left: `${s.displayPercent}%`,
2322
+ transform: `translate(-50%, calc(-50% + ${s.verticalOffset}px)) scale(${s.thumbScale}) scaleX(${s.stretchX}) scaleY(${s.stretchY})`,
2323
+ transformOrigin: `${s.stretchOriginPercent}% center`,
2324
+ boxShadow: `0 ${s.shadowY}px ${s.shadowBlur}px rgba(0, 0, 0, ${s.shadowOpacity})`
2325
+ },
2326
+ onPointerDown: handlePointerDown,
2327
+ onPointerMove: handlePointerMove,
2328
+ onPointerUp: handlePointerUp,
2329
+ onPointerCancel: handlePointerUp
2330
+ }
2331
+ )
2332
+ ]
2333
+ }
2334
+ )
2335
+ ] });
2336
+ }
2337
+ function createThumbState(percent) {
2338
+ return {
2339
+ displayPercent: percent,
2340
+ thumbScale: 1,
2341
+ verticalOffset: 0,
2342
+ stretchX: 1,
2343
+ stretchY: 1,
2344
+ stretchOriginPercent: 50,
2345
+ targetOriginPercent: 50,
2346
+ shadowY: 2,
2347
+ shadowBlur: 6,
2348
+ shadowOpacity: 0.12,
2349
+ lastPercent: percent,
2350
+ isDragging: false
2351
+ };
2352
+ }
2353
+ function PhysicsRangeSlider({
2354
+ label,
2355
+ description,
2356
+ minValue,
2357
+ maxValue,
2358
+ min,
2359
+ max,
2360
+ step,
2361
+ config,
2362
+ onChange,
2363
+ formatValue = (v) => v.toFixed(0),
2364
+ className = ""
2365
+ }) {
2366
+ const trackRef = useRef(null);
2367
+ const minPercent = (minValue - min) / (max - min) * 100;
2368
+ const maxPercent = (maxValue - min) / (max - min) * 100;
2369
+ const minThumb = useRef(createThumbState(minPercent));
2370
+ const maxThumb = useRef(createThumbState(maxPercent));
2371
+ const activeThumb = useRef(null);
2372
+ const pointerStart = useRef(null);
2373
+ const [, forceRender] = React.useState(0);
2374
+ const rerender = useCallback(() => forceRender((n) => n + 1), []);
2375
+ const minScaleId = useRef(generatePhysicsId("rangeMinScale"));
2376
+ const maxScaleId = useRef(generatePhysicsId("rangeMaxScale"));
2377
+ const minDecayId = useRef(generatePhysicsId("rangeMinDecay"));
2378
+ const maxDecayId = useRef(generatePhysicsId("rangeMaxDecay"));
2379
+ const minRelaxId = useRef(generatePhysicsId("rangeMinRelax"));
2380
+ const maxRelaxId = useRef(generatePhysicsId("rangeMaxRelax"));
2381
+ const minScaleSpring = useRef(null);
2382
+ const maxScaleSpring = useRef(null);
2383
+ useEffect(() => {
2384
+ if (!minThumb.current.isDragging) {
2385
+ minThumb.current.displayPercent = (minValue - min) / (max - min) * 100;
2386
+ }
2387
+ if (!maxThumb.current.isDragging) {
2388
+ maxThumb.current.displayPercent = (maxValue - min) / (max - min) * 100;
2389
+ }
2390
+ rerender();
2391
+ }, [minValue, maxValue, min, max, rerender]);
2392
+ const percentToValue = useCallback(
2393
+ (percent) => {
2394
+ const raw = min + percent / 100 * (max - min);
2395
+ return Math.round(raw / step) * step;
2396
+ },
2397
+ [min, max, step]
2398
+ );
2399
+ const animateScaleTo = useCallback(
2400
+ (thumb, targetScale) => {
2401
+ const state = thumb === "min" ? minThumb.current : maxThumb.current;
2402
+ const springRef = thumb === "min" ? minScaleSpring : maxScaleSpring;
2403
+ const scaleId = thumb === "min" ? minScaleId : maxScaleId;
2404
+ PhysicsEngine.unregister(scaleId.current);
2405
+ springRef.current = new Spring1D(state.thumbScale, {
2406
+ tension: 400,
2407
+ friction: 20,
2408
+ mass: 0.6,
2409
+ precision: 1e-3
2410
+ });
2411
+ springRef.current.setTarget(targetScale);
2412
+ PhysicsEngine.register(scaleId.current, (dt) => {
2413
+ if (!springRef.current) return;
2414
+ const result = springRef.current.step(dt);
2415
+ state.thumbScale = result.value;
2416
+ rerender();
2417
+ if (result.isSettled) {
2418
+ PhysicsEngine.unregister(scaleId.current);
2419
+ }
2420
+ });
2421
+ },
2422
+ [rerender]
2423
+ );
2424
+ const animateStretchRelax = useCallback(
2425
+ (thumb) => {
2426
+ const state = thumb === "min" ? minThumb.current : maxThumb.current;
2427
+ const relaxId = thumb === "min" ? minRelaxId : maxRelaxId;
2428
+ PhysicsEngine.unregister(relaxId.current);
2429
+ const spring = new Spring1D(state.stretchX, {
2430
+ tension: 300,
2431
+ friction: 14,
2432
+ mass: 0.5,
2433
+ precision: 1e-3
2434
+ });
2435
+ spring.setTarget(1);
2436
+ PhysicsEngine.register(relaxId.current, (dt) => {
2437
+ const result = spring.step(dt);
2438
+ state.stretchX = result.value;
2439
+ state.stretchY = 1 - (result.value - 1) * 0.4;
2440
+ state.stretchOriginPercent += (50 - state.stretchOriginPercent) * 0.15;
2441
+ state.shadowY += (2 - state.shadowY) * 0.15;
2442
+ state.shadowBlur += (6 - state.shadowBlur) * 0.15;
2443
+ state.shadowOpacity += (0.12 - state.shadowOpacity) * 0.15;
2444
+ rerender();
2445
+ if (result.isSettled) {
2446
+ state.stretchX = 1;
2447
+ state.stretchY = 1;
2448
+ state.stretchOriginPercent = 50;
2449
+ PhysicsEngine.unregister(relaxId.current);
2450
+ }
2451
+ });
2452
+ },
2453
+ [rerender]
2454
+ );
2455
+ const handlePointerDown = useCallback(
2456
+ (e, thumb) => {
2457
+ if (!trackRef.current) return;
2458
+ const state = thumb === "min" ? minThumb.current : maxThumb.current;
2459
+ const decayId = thumb === "min" ? minDecayId : maxDecayId;
2460
+ e.preventDefault();
2461
+ e.stopPropagation();
2462
+ e.target.setPointerCapture(e.pointerId);
2463
+ pointerStart.current = { x: e.clientX, y: e.clientY };
2464
+ activeThumb.current = thumb;
2465
+ state.isDragging = true;
2466
+ animateScaleTo(thumb, config.liftScale);
2467
+ PhysicsEngine.register(decayId.current, () => {
2468
+ if (!state.isDragging) return;
2469
+ const decayLerp = 0.08;
2470
+ const threshold = 1e-3;
2471
+ if (Math.abs(state.stretchX - 1) > threshold) {
2472
+ state.stretchX += (1 - state.stretchX) * decayLerp;
2473
+ }
2474
+ if (Math.abs(state.stretchY - 1) > threshold) {
2475
+ state.stretchY += (1 - state.stretchY) * decayLerp;
2476
+ }
2477
+ if (Math.abs(state.stretchOriginPercent - 50) > threshold) {
2478
+ state.stretchOriginPercent += (50 - state.stretchOriginPercent) * decayLerp;
2479
+ }
2480
+ state.shadowY += (2 - state.shadowY) * decayLerp;
2481
+ state.shadowBlur += (6 - state.shadowBlur) * decayLerp;
2482
+ state.shadowOpacity += (0.12 - state.shadowOpacity) * decayLerp;
2483
+ rerender();
2484
+ });
2485
+ rerender();
2486
+ },
2487
+ [config.liftScale, animateScaleTo, rerender]
2488
+ );
2489
+ const handlePointerMove = useCallback(
2490
+ (e) => {
2491
+ if (!activeThumb.current || !trackRef.current || !pointerStart.current) return;
2492
+ const thumb = activeThumb.current;
2493
+ const state = thumb === "min" ? minThumb.current : maxThumb.current;
2494
+ const rect = trackRef.current.getBoundingClientRect();
2495
+ const rawX = e.clientX - rect.left;
2496
+ let percent = rawX / rect.width * 100;
2497
+ if (thumb === "min") {
2498
+ percent = Math.max(0, Math.min(maxThumb.current.displayPercent - 5, percent));
2499
+ } else {
2500
+ percent = Math.min(100, Math.max(minThumb.current.displayPercent + 5, percent));
2501
+ }
2502
+ const velocity = percent - state.lastPercent;
2503
+ state.lastPercent = percent;
2504
+ if (Math.abs(velocity) > 0.3) {
2505
+ state.targetOriginPercent = velocity > 0 ? 100 : 0;
2506
+ }
2507
+ state.stretchOriginPercent += (state.targetOriginPercent - state.stretchOriginPercent) * 0.15;
2508
+ const absVelocity = Math.abs(velocity);
2509
+ const velStretch = Math.min(absVelocity * config.velocityStretch * 15, 1.2);
2510
+ const targetStretchX = 1 + velStretch;
2511
+ const targetStretchY = 1 - velStretch * 0.4;
2512
+ const baseLerp = 0.35;
2513
+ const decayLerp = 0.15;
2514
+ const velocityThreshold = 0.5;
2515
+ if (absVelocity < velocityThreshold) {
2516
+ const decayFactor = 1 - absVelocity / velocityThreshold;
2517
+ state.stretchX += (1 - state.stretchX) * decayLerp * decayFactor;
2518
+ state.stretchY += (1 - state.stretchY) * decayLerp * decayFactor;
2519
+ state.stretchOriginPercent += (50 - state.stretchOriginPercent) * decayLerp * decayFactor;
2520
+ }
2521
+ state.stretchX += (targetStretchX - state.stretchX) * baseLerp;
2522
+ state.stretchY += (targetStretchY - state.stretchY) * baseLerp;
2523
+ const targetShadowY = 2 + absVelocity * 0.4;
2524
+ const targetShadowBlur = 6 + absVelocity * 0.6;
2525
+ const targetShadowOpacity = 0.12 + absVelocity * 0.015;
2526
+ state.shadowY += (targetShadowY - state.shadowY) * 0.3;
2527
+ state.shadowBlur += (targetShadowBlur - state.shadowBlur) * 0.3;
2528
+ state.shadowOpacity += (targetShadowOpacity - state.shadowOpacity) * 0.3;
2529
+ state.displayPercent = percent;
2530
+ const newMin = thumb === "min" ? percentToValue(percent) : minValue;
2531
+ const newMax = thumb === "max" ? percentToValue(percent) : maxValue;
2532
+ onChange(newMin, newMax);
2533
+ rerender();
2534
+ },
2535
+ [config.velocityStretch, minValue, maxValue, percentToValue, onChange, rerender]
2536
+ );
2537
+ const handlePointerUp = useCallback(() => {
2538
+ if (!activeThumb.current) return;
2539
+ const thumb = activeThumb.current;
2540
+ const state = thumb === "min" ? minThumb.current : maxThumb.current;
2541
+ const decayId = thumb === "min" ? minDecayId : maxDecayId;
2542
+ state.isDragging = false;
2543
+ pointerStart.current = null;
2544
+ activeThumb.current = null;
2545
+ PhysicsEngine.unregister(decayId.current);
2546
+ animateScaleTo(thumb, 1);
2547
+ animateStretchRelax(thumb);
2548
+ rerender();
2549
+ }, [animateScaleTo, animateStretchRelax, rerender]);
2550
+ useEffect(() => {
2551
+ return () => {
2552
+ PhysicsEngine.unregister(minScaleId.current);
2553
+ PhysicsEngine.unregister(maxScaleId.current);
2554
+ PhysicsEngine.unregister(minDecayId.current);
2555
+ PhysicsEngine.unregister(maxDecayId.current);
2556
+ PhysicsEngine.unregister(minRelaxId.current);
2557
+ PhysicsEngine.unregister(maxRelaxId.current);
2558
+ };
2559
+ }, []);
2560
+ const minS = minThumb.current;
2561
+ const maxS = maxThumb.current;
2562
+ return /* @__PURE__ */ jsxs("div", { className: `tekiyo-range-slider ${className}`, children: [
2563
+ /* @__PURE__ */ jsxs("div", { className: "tekiyo-slider-header", children: [
2564
+ /* @__PURE__ */ jsxs("div", { className: "tekiyo-slider-label-group", children: [
2565
+ /* @__PURE__ */ jsx("span", { className: "tekiyo-slider-label", children: label }),
2566
+ description && /* @__PURE__ */ jsx("span", { className: "tekiyo-slider-description", children: description })
2567
+ ] }),
2568
+ /* @__PURE__ */ jsxs("span", { className: "tekiyo-slider-value", children: [
2569
+ formatValue(minValue),
2570
+ " - ",
2571
+ formatValue(maxValue)
2572
+ ] })
2573
+ ] }),
2574
+ /* @__PURE__ */ jsxs(
2575
+ "div",
2576
+ {
2577
+ className: "tekiyo-slider-track",
2578
+ ref: trackRef,
2579
+ onPointerMove: handlePointerMove,
2580
+ onPointerUp: handlePointerUp,
2581
+ onPointerCancel: handlePointerUp,
2582
+ children: [
2583
+ /* @__PURE__ */ jsx("div", { className: "tekiyo-slider-track-bg" }),
2584
+ /* @__PURE__ */ jsx(
2585
+ "div",
2586
+ {
2587
+ className: "tekiyo-range-fill",
2588
+ style: {
2589
+ left: `${minS.displayPercent}%`,
2590
+ width: `${maxS.displayPercent - minS.displayPercent}%`
2591
+ }
2592
+ }
2593
+ ),
2594
+ /* @__PURE__ */ jsx(
2595
+ "div",
2596
+ {
2597
+ className: `tekiyo-slider-thumb tekiyo-range-thumb-min ${minS.isDragging ? "active" : ""}`,
2598
+ style: {
2599
+ left: `${minS.displayPercent}%`,
2600
+ transform: `translate(-50%, calc(-50% + ${minS.verticalOffset}px)) scale(${minS.thumbScale}) scaleX(${minS.stretchX}) scaleY(${minS.stretchY})`,
2601
+ transformOrigin: `${minS.stretchOriginPercent}% center`,
2602
+ boxShadow: `0 ${minS.shadowY}px ${minS.shadowBlur}px rgba(0, 0, 0, ${minS.shadowOpacity})`
2603
+ },
2604
+ onPointerDown: (e) => handlePointerDown(e, "min")
2605
+ }
2606
+ ),
2607
+ /* @__PURE__ */ jsx(
2608
+ "div",
2609
+ {
2610
+ className: `tekiyo-slider-thumb tekiyo-range-thumb-max ${maxS.isDragging ? "active" : ""}`,
2611
+ style: {
2612
+ left: `${maxS.displayPercent}%`,
2613
+ transform: `translate(-50%, calc(-50% + ${maxS.verticalOffset}px)) scale(${maxS.thumbScale}) scaleX(${maxS.stretchX}) scaleY(${maxS.stretchY})`,
2614
+ transformOrigin: `${maxS.stretchOriginPercent}% center`,
2615
+ boxShadow: `0 ${maxS.shadowY}px ${maxS.shadowBlur}px rgba(0, 0, 0, ${maxS.shadowOpacity})`
2616
+ },
2617
+ onPointerDown: (e) => handlePointerDown(e, "max")
2618
+ }
2619
+ )
2620
+ ]
2621
+ }
2622
+ )
2623
+ ] });
2624
+ }
2625
+ function PhysicsStepSlider({
2626
+ label,
2627
+ description,
2628
+ options,
2629
+ value,
2630
+ config,
2631
+ onChange,
2632
+ className = ""
2633
+ }) {
2634
+ const trackRef = useRef(null);
2635
+ const currentIndex = options.findIndex((opt) => opt.value === value);
2636
+ const stepPercent = 100 / (options.length - 1);
2637
+ const targetPercent = currentIndex * stepPercent;
2638
+ const state = useRef({
2639
+ isDragging: false,
2640
+ displayPercent: targetPercent,
2641
+ thumbScale: 1,
2642
+ verticalOffset: 0,
2643
+ stretchX: 1,
2644
+ stretchY: 1,
2645
+ stretchOriginPercent: 50,
2646
+ targetOriginPercent: 50,
2647
+ shadowY: 2,
2648
+ shadowBlur: 6,
2649
+ shadowOpacity: 0.12,
2650
+ lastPercent: targetPercent,
2651
+ pointerStart: null
2652
+ });
2653
+ const [, forceRender] = React.useState(0);
2654
+ const rerender = useCallback(() => forceRender((n) => n + 1), []);
2655
+ const scaleIdRef = useRef(generatePhysicsId("stepScale"));
2656
+ const snapIdRef = useRef(generatePhysicsId("stepSnap"));
2657
+ const decayIdRef = useRef(generatePhysicsId("stepDecay"));
2658
+ const relaxIdRef = useRef(generatePhysicsId("stepRelax"));
2659
+ const scaleSpring = useRef(null);
2660
+ const snapSpring = useRef(null);
2661
+ useEffect(() => {
2662
+ if (!state.current.isDragging) {
2663
+ const idx = options.findIndex((opt) => opt.value === value);
2664
+ const percent = idx * stepPercent;
2665
+ animateToPercent(percent);
2666
+ }
2667
+ }, [value, options, stepPercent]);
2668
+ const animateToPercent = useCallback(
2669
+ (targetPercent2) => {
2670
+ const s2 = state.current;
2671
+ PhysicsEngine.unregister(snapIdRef.current);
2672
+ snapSpring.current = new Spring1D(s2.displayPercent, {
2673
+ tension: config.springTension * 500,
2674
+ friction: 22,
2675
+ mass: 0.8,
2676
+ precision: 0.01
2677
+ });
2678
+ snapSpring.current.setTarget(targetPercent2);
2679
+ PhysicsEngine.register(snapIdRef.current, (dt) => {
2680
+ if (!snapSpring.current) return;
2681
+ const result = snapSpring.current.step(dt);
2682
+ s2.displayPercent = result.value;
2683
+ rerender();
2684
+ if (result.isSettled) {
2685
+ PhysicsEngine.unregister(snapIdRef.current);
2686
+ }
2687
+ });
2688
+ },
2689
+ [config.springTension, rerender]
2690
+ );
2691
+ const animateScaleTo = useCallback(
2692
+ (targetScale) => {
2693
+ const s2 = state.current;
2694
+ PhysicsEngine.unregister(scaleIdRef.current);
2695
+ scaleSpring.current = new Spring1D(s2.thumbScale, {
2696
+ tension: 400,
2697
+ friction: 20,
2698
+ mass: 0.6,
2699
+ precision: 1e-3
2700
+ });
2701
+ scaleSpring.current.setTarget(targetScale);
2702
+ PhysicsEngine.register(scaleIdRef.current, (dt) => {
2703
+ if (!scaleSpring.current) return;
2704
+ const result = scaleSpring.current.step(dt);
2705
+ s2.thumbScale = result.value;
2706
+ rerender();
2707
+ if (result.isSettled) {
2708
+ PhysicsEngine.unregister(scaleIdRef.current);
2709
+ }
2710
+ });
2711
+ },
2712
+ [rerender]
2713
+ );
2714
+ const animateStretchRelax = useCallback(() => {
2715
+ const s2 = state.current;
2716
+ PhysicsEngine.unregister(relaxIdRef.current);
2717
+ const spring = new Spring1D(s2.stretchX, {
2718
+ tension: 300,
2719
+ friction: 14,
2720
+ mass: 0.5,
2721
+ precision: 1e-3
2722
+ });
2723
+ spring.setTarget(1);
2724
+ PhysicsEngine.register(relaxIdRef.current, (dt) => {
2725
+ const result = spring.step(dt);
2726
+ s2.stretchX = result.value;
2727
+ s2.stretchY = 1 - (result.value - 1) * 0.4;
2728
+ s2.stretchOriginPercent += (50 - s2.stretchOriginPercent) * 0.15;
2729
+ s2.shadowY += (2 - s2.shadowY) * 0.15;
2730
+ s2.shadowBlur += (6 - s2.shadowBlur) * 0.15;
2731
+ s2.shadowOpacity += (0.12 - s2.shadowOpacity) * 0.15;
2732
+ rerender();
2733
+ if (result.isSettled) {
2734
+ s2.stretchX = 1;
2735
+ s2.stretchY = 1;
2736
+ s2.stretchOriginPercent = 50;
2737
+ PhysicsEngine.unregister(relaxIdRef.current);
2738
+ }
2739
+ });
2740
+ }, [rerender]);
2741
+ const getClosestStepIndex = useCallback(
2742
+ (percent) => {
2743
+ const stepSize = 100 / (options.length - 1);
2744
+ return Math.round(percent / stepSize);
2745
+ },
2746
+ [options.length]
2747
+ );
2748
+ const handlePointerDown = useCallback(
2749
+ (e) => {
2750
+ if (!trackRef.current) return;
2751
+ const s2 = state.current;
2752
+ e.preventDefault();
2753
+ e.target.setPointerCapture(e.pointerId);
2754
+ s2.pointerStart = { x: e.clientX, y: e.clientY };
2755
+ s2.isDragging = true;
2756
+ PhysicsEngine.unregister(snapIdRef.current);
2757
+ animateScaleTo(config.liftScale);
2758
+ PhysicsEngine.register(decayIdRef.current, () => {
2759
+ if (!s2.isDragging) return;
2760
+ const decayLerp = 0.08;
2761
+ const threshold = 1e-3;
2762
+ if (Math.abs(s2.stretchX - 1) > threshold) {
2763
+ s2.stretchX += (1 - s2.stretchX) * decayLerp;
2764
+ }
2765
+ if (Math.abs(s2.stretchY - 1) > threshold) {
2766
+ s2.stretchY += (1 - s2.stretchY) * decayLerp;
2767
+ }
2768
+ if (Math.abs(s2.stretchOriginPercent - 50) > threshold) {
2769
+ s2.stretchOriginPercent += (50 - s2.stretchOriginPercent) * decayLerp;
2770
+ }
2771
+ s2.shadowY += (2 - s2.shadowY) * decayLerp;
2772
+ s2.shadowBlur += (6 - s2.shadowBlur) * decayLerp;
2773
+ s2.shadowOpacity += (0.12 - s2.shadowOpacity) * decayLerp;
2774
+ rerender();
2775
+ });
2776
+ const rect = trackRef.current.getBoundingClientRect();
2777
+ const x = Math.max(0, Math.min(rect.width, e.clientX - rect.left));
2778
+ const percent = x / rect.width * 100;
2779
+ s2.displayPercent = percent;
2780
+ s2.lastPercent = percent;
2781
+ rerender();
2782
+ },
2783
+ [config.liftScale, animateScaleTo, rerender]
2784
+ );
2785
+ const handlePointerMove = useCallback(
2786
+ (e) => {
2787
+ const s2 = state.current;
2788
+ if (!s2.isDragging || !trackRef.current || !s2.pointerStart) return;
2789
+ const rect = trackRef.current.getBoundingClientRect();
2790
+ const rawX = e.clientX - rect.left;
2791
+ const percent = Math.max(0, Math.min(100, rawX / rect.width * 100));
2792
+ const velocity = percent - s2.lastPercent;
2793
+ s2.lastPercent = percent;
2794
+ if (Math.abs(velocity) > 0.3) {
2795
+ s2.targetOriginPercent = velocity > 0 ? 100 : 0;
2796
+ }
2797
+ s2.stretchOriginPercent += (s2.targetOriginPercent - s2.stretchOriginPercent) * 0.15;
2798
+ const absVelocity = Math.abs(velocity);
2799
+ const velStretch = Math.min(absVelocity * config.velocityStretch * 15, 1.2);
2800
+ const targetStretchX = 1 + velStretch;
2801
+ const targetStretchY = 1 - velStretch * 0.4;
2802
+ const baseLerp = 0.35;
2803
+ const decayLerp = 0.15;
2804
+ const velocityThreshold = 0.5;
2805
+ if (absVelocity < velocityThreshold) {
2806
+ const decayFactor = 1 - absVelocity / velocityThreshold;
2807
+ s2.stretchX += (1 - s2.stretchX) * decayLerp * decayFactor;
2808
+ s2.stretchY += (1 - s2.stretchY) * decayLerp * decayFactor;
2809
+ }
2810
+ s2.stretchX += (targetStretchX - s2.stretchX) * baseLerp;
2811
+ s2.stretchY += (targetStretchY - s2.stretchY) * baseLerp;
2812
+ const targetShadowY = 2 + absVelocity * 0.4;
2813
+ const targetShadowBlur = 6 + absVelocity * 0.6;
2814
+ const targetShadowOpacity = 0.12 + absVelocity * 0.015;
2815
+ s2.shadowY += (targetShadowY - s2.shadowY) * 0.3;
2816
+ s2.shadowBlur += (targetShadowBlur - s2.shadowBlur) * 0.3;
2817
+ s2.shadowOpacity += (targetShadowOpacity - s2.shadowOpacity) * 0.3;
2818
+ s2.displayPercent = percent;
2819
+ rerender();
2820
+ },
2821
+ [config.velocityStretch, rerender]
2822
+ );
2823
+ const handlePointerUp = useCallback(() => {
2824
+ var _a;
2825
+ const s2 = state.current;
2826
+ if (!s2.isDragging) return;
2827
+ s2.isDragging = false;
2828
+ s2.pointerStart = null;
2829
+ PhysicsEngine.unregister(decayIdRef.current);
2830
+ animateScaleTo(1);
2831
+ animateStretchRelax();
2832
+ const closestIndex = getClosestStepIndex(s2.displayPercent);
2833
+ const snapPercent = closestIndex * stepPercent;
2834
+ animateToPercent(snapPercent);
2835
+ const newValue = (_a = options[closestIndex]) == null ? void 0 : _a.value;
2836
+ if (newValue !== void 0 && newValue !== value) {
2837
+ onChange(newValue);
2838
+ }
2839
+ rerender();
2840
+ }, [animateScaleTo, animateStretchRelax, animateToPercent, getClosestStepIndex, stepPercent, options, value, onChange, rerender]);
2841
+ const handleStepClick = useCallback(
2842
+ (index) => {
2843
+ if (state.current.isDragging) return;
2844
+ const newValue = options[index].value;
2845
+ if (newValue !== value) {
2846
+ onChange(newValue);
2847
+ }
2848
+ },
2849
+ [options, value, onChange]
2850
+ );
2851
+ useEffect(() => {
2852
+ return () => {
2853
+ PhysicsEngine.unregister(scaleIdRef.current);
2854
+ PhysicsEngine.unregister(snapIdRef.current);
2855
+ PhysicsEngine.unregister(decayIdRef.current);
2856
+ PhysicsEngine.unregister(relaxIdRef.current);
2857
+ };
2858
+ }, []);
2859
+ const s = state.current;
2860
+ const currentOption = options[currentIndex];
2861
+ return /* @__PURE__ */ jsxs("div", { className: `tekiyo-step-slider ${className}`, children: [
2862
+ /* @__PURE__ */ jsxs("div", { className: "tekiyo-slider-header", children: [
2863
+ /* @__PURE__ */ jsxs("div", { className: "tekiyo-slider-label-group", children: [
2864
+ /* @__PURE__ */ jsx("span", { className: "tekiyo-slider-label", children: label }),
2865
+ description && /* @__PURE__ */ jsx("span", { className: "tekiyo-slider-description", children: description })
2866
+ ] }),
2867
+ /* @__PURE__ */ jsx(
2868
+ "span",
2869
+ {
2870
+ className: "tekiyo-step-value",
2871
+ style: { color: currentOption == null ? void 0 : currentOption.color },
2872
+ children: currentOption == null ? void 0 : currentOption.label
2873
+ }
2874
+ )
2875
+ ] }),
2876
+ /* @__PURE__ */ jsxs(
2877
+ "div",
2878
+ {
2879
+ className: "tekiyo-step-track",
2880
+ ref: trackRef,
2881
+ onPointerMove: handlePointerMove,
2882
+ onPointerUp: handlePointerUp,
2883
+ onPointerCancel: handlePointerUp,
2884
+ children: [
2885
+ /* @__PURE__ */ jsx("div", { className: "tekiyo-step-track-bg" }),
2886
+ /* @__PURE__ */ jsx(
2887
+ "div",
2888
+ {
2889
+ className: "tekiyo-step-track-fill",
2890
+ style: {
2891
+ width: `${s.displayPercent}%`,
2892
+ background: (currentOption == null ? void 0 : currentOption.color) || "#007aff"
2893
+ }
2894
+ }
2895
+ ),
2896
+ /* @__PURE__ */ jsx("div", { className: "tekiyo-step-dots", children: options.map((option, index) => {
2897
+ const dotPercent = index * stepPercent;
2898
+ const isActive = s.displayPercent >= dotPercent - 1;
2899
+ return /* @__PURE__ */ jsx(
2900
+ "div",
2901
+ {
2902
+ className: `tekiyo-step-dot ${isActive ? "active" : ""}`,
2903
+ style: {
2904
+ left: `${dotPercent}%`,
2905
+ background: isActive ? option.color || "#007aff" : void 0
2906
+ },
2907
+ onClick: () => handleStepClick(index)
2908
+ },
2909
+ option.value
2910
+ );
2911
+ }) }),
2912
+ /* @__PURE__ */ jsx(
2913
+ "div",
2914
+ {
2915
+ className: `tekiyo-slider-thumb tekiyo-step-thumb ${s.isDragging ? "active" : ""}`,
2916
+ style: {
2917
+ left: `${s.displayPercent}%`,
2918
+ transform: `translate(-50%, calc(-50% + ${s.verticalOffset}px)) scale(${s.thumbScale}) scaleX(${s.stretchX}) scaleY(${s.stretchY})`,
2919
+ transformOrigin: `${s.stretchOriginPercent}% center`,
2920
+ boxShadow: `0 ${s.shadowY}px ${s.shadowBlur}px rgba(0, 0, 0, ${s.shadowOpacity})`
2921
+ },
2922
+ onPointerDown: handlePointerDown
2923
+ }
2924
+ )
2925
+ ]
2926
+ }
2927
+ ),
2928
+ /* @__PURE__ */ jsx("div", { className: "tekiyo-step-labels", children: options.map((option, index) => {
2929
+ const labelPercent = index * stepPercent;
2930
+ const isSelected = currentIndex === index;
2931
+ return /* @__PURE__ */ jsx(
2932
+ "span",
2933
+ {
2934
+ className: `tekiyo-step-label ${isSelected ? "selected" : ""}`,
2935
+ style: {
2936
+ left: `${labelPercent}%`,
2937
+ color: isSelected ? option.color || "#007aff" : void 0
2938
+ },
2939
+ onClick: () => handleStepClick(index),
2940
+ children: option.label
2941
+ },
2942
+ option.value
2943
+ );
2944
+ }) })
2945
+ ] });
2946
+ }
2947
+ function PhysicsVerticalSlider({
2948
+ label,
2949
+ icon,
2950
+ value,
2951
+ min,
2952
+ max,
2953
+ step,
2954
+ config,
2955
+ onChange,
2956
+ height = 160,
2957
+ color = "#007aff",
2958
+ className = ""
2959
+ }) {
2960
+ const trackRef = useRef(null);
2961
+ const state = useRef({
2962
+ isDragging: false,
2963
+ displayPercent: (value - min) / (max - min) * 100,
2964
+ thumbScale: 1,
2965
+ horizontalOffset: 0,
2966
+ stretchX: 1,
2967
+ stretchY: 1,
2968
+ stretchOriginPercent: 50,
2969
+ targetOriginPercent: 50,
2970
+ shadowX: 0,
2971
+ shadowY: 2,
2972
+ shadowBlur: 6,
2973
+ shadowOpacity: 0.12,
2974
+ lastPercent: (value - min) / (max - min) * 100,
2975
+ pointerStart: null
2976
+ });
2977
+ const [, forceRender] = React.useState(0);
2978
+ const rerender = useCallback(() => forceRender((n) => n + 1), []);
2979
+ const physicsIdRef = useRef(generatePhysicsId("vslider"));
2980
+ const scaleIdRef = useRef(generatePhysicsId("vscale"));
2981
+ const decayIdRef = useRef(generatePhysicsId("vdecay"));
2982
+ const relaxIdRef = useRef(generatePhysicsId("vrelax"));
2983
+ const horizontalIdRef = useRef(generatePhysicsId("vhorizontal"));
2984
+ const springRef = useRef(null);
2985
+ const scaleSpringRef = useRef(null);
2986
+ const horizontalSpringRef = useRef(null);
2987
+ useEffect(() => {
2988
+ if (!state.current.isDragging) {
2989
+ const targetPercent = (value - min) / (max - min) * 100;
2990
+ animateToPercent(targetPercent);
2991
+ }
2992
+ }, [value, min, max]);
2993
+ const animateToPercent = useCallback(
2994
+ (targetPercent) => {
2995
+ const s2 = state.current;
2996
+ PhysicsEngine.unregister(physicsIdRef.current);
2997
+ springRef.current = new Spring1D(s2.displayPercent, {
2998
+ tension: config.springTension * 400,
2999
+ friction: 26,
3000
+ mass: 1,
3001
+ precision: 0.01
3002
+ });
3003
+ springRef.current.setTarget(targetPercent);
3004
+ PhysicsEngine.register(physicsIdRef.current, (dt) => {
3005
+ if (!springRef.current) return;
3006
+ const result = springRef.current.step(dt);
3007
+ s2.displayPercent = result.value;
3008
+ rerender();
3009
+ if (result.isSettled) {
3010
+ PhysicsEngine.unregister(physicsIdRef.current);
3011
+ }
3012
+ });
3013
+ },
3014
+ [config.springTension, rerender]
3015
+ );
3016
+ const percentToValue = useCallback(
3017
+ (percent) => {
3018
+ const raw = min + percent / 100 * (max - min);
3019
+ return Math.round(raw / step) * step;
3020
+ },
3021
+ [min, max, step]
3022
+ );
3023
+ const animateScaleTo = useCallback(
3024
+ (targetScale) => {
3025
+ const s2 = state.current;
3026
+ PhysicsEngine.unregister(scaleIdRef.current);
3027
+ scaleSpringRef.current = new Spring1D(s2.thumbScale, {
3028
+ tension: 400,
3029
+ friction: 20,
3030
+ mass: 0.6,
3031
+ precision: 1e-3
3032
+ });
3033
+ scaleSpringRef.current.setTarget(targetScale);
3034
+ PhysicsEngine.register(scaleIdRef.current, (dt) => {
3035
+ if (!scaleSpringRef.current) return;
3036
+ const result = scaleSpringRef.current.step(dt);
3037
+ s2.thumbScale = result.value;
3038
+ rerender();
3039
+ if (result.isSettled) {
3040
+ PhysicsEngine.unregister(scaleIdRef.current);
3041
+ }
3042
+ });
3043
+ },
3044
+ [rerender]
3045
+ );
3046
+ const animateStretchRelax = useCallback(() => {
3047
+ const s2 = state.current;
3048
+ PhysicsEngine.unregister(relaxIdRef.current);
3049
+ const spring = new Spring1D(s2.stretchY, {
3050
+ tension: 300,
3051
+ friction: 14,
3052
+ mass: 0.5,
3053
+ precision: 1e-3
3054
+ });
3055
+ spring.setTarget(1);
3056
+ PhysicsEngine.register(relaxIdRef.current, (dt) => {
3057
+ const result = spring.step(dt);
3058
+ s2.stretchY = result.value;
3059
+ s2.stretchX = 1 - (result.value - 1) * 0.4;
3060
+ s2.stretchOriginPercent += (50 - s2.stretchOriginPercent) * 0.15;
3061
+ s2.shadowY += (2 - s2.shadowY) * 0.15;
3062
+ s2.shadowBlur += (6 - s2.shadowBlur) * 0.15;
3063
+ s2.shadowOpacity += (0.12 - s2.shadowOpacity) * 0.15;
3064
+ rerender();
3065
+ if (result.isSettled) {
3066
+ s2.stretchX = 1;
3067
+ s2.stretchY = 1;
3068
+ s2.stretchOriginPercent = 50;
3069
+ PhysicsEngine.unregister(relaxIdRef.current);
3070
+ }
3071
+ });
3072
+ }, [rerender]);
3073
+ const handlePointerDown = useCallback(
3074
+ (e) => {
3075
+ if (!trackRef.current) return;
3076
+ const s2 = state.current;
3077
+ e.preventDefault();
3078
+ e.target.setPointerCapture(e.pointerId);
3079
+ s2.pointerStart = { x: e.clientX, y: e.clientY };
3080
+ s2.isDragging = true;
3081
+ PhysicsEngine.unregister(physicsIdRef.current);
3082
+ PhysicsEngine.unregister(horizontalIdRef.current);
3083
+ PhysicsEngine.unregister(relaxIdRef.current);
3084
+ animateScaleTo(config.liftScale);
3085
+ PhysicsEngine.register(decayIdRef.current, () => {
3086
+ if (!s2.isDragging) return;
3087
+ const decayLerp = 0.08;
3088
+ const threshold = 1e-3;
3089
+ if (Math.abs(s2.stretchY - 1) > threshold) {
3090
+ s2.stretchY += (1 - s2.stretchY) * decayLerp;
3091
+ }
3092
+ if (Math.abs(s2.stretchX - 1) > threshold) {
3093
+ s2.stretchX += (1 - s2.stretchX) * decayLerp;
3094
+ }
3095
+ if (Math.abs(s2.stretchOriginPercent - 50) > threshold) {
3096
+ s2.stretchOriginPercent += (50 - s2.stretchOriginPercent) * decayLerp;
3097
+ }
3098
+ s2.shadowY += (2 - s2.shadowY) * decayLerp;
3099
+ s2.shadowBlur += (6 - s2.shadowBlur) * decayLerp;
3100
+ s2.shadowOpacity += (0.12 - s2.shadowOpacity) * decayLerp;
3101
+ rerender();
3102
+ });
3103
+ const rect = trackRef.current.getBoundingClientRect();
3104
+ const y = Math.max(0, Math.min(rect.height, e.clientY - rect.top));
3105
+ const percent = 100 - y / rect.height * 100;
3106
+ s2.displayPercent = percent;
3107
+ s2.lastPercent = percent;
3108
+ onChange(percentToValue(percent));
3109
+ rerender();
3110
+ },
3111
+ [config.liftScale, animateScaleTo, percentToValue, onChange, rerender]
3112
+ );
3113
+ const handlePointerMove = useCallback(
3114
+ (e) => {
3115
+ const s2 = state.current;
3116
+ if (!s2.isDragging || !trackRef.current || !s2.pointerStart) return;
3117
+ const rect = trackRef.current.getBoundingClientRect();
3118
+ const rawY = e.clientY - rect.top;
3119
+ const rawPercent = 100 - rawY / rect.height * 100;
3120
+ let percent = rawPercent;
3121
+ let edgeStretchV = 0;
3122
+ if (rawPercent < 0) {
3123
+ edgeStretchV = -rawPercent / 100;
3124
+ percent = rawPercent * 0.15;
3125
+ s2.targetOriginPercent = 0;
3126
+ } else if (rawPercent > 100) {
3127
+ edgeStretchV = (rawPercent - 100) / 100;
3128
+ percent = 100 + (rawPercent - 100) * 0.15;
3129
+ s2.targetOriginPercent = 100;
3130
+ }
3131
+ const velocity = percent - s2.lastPercent;
3132
+ s2.lastPercent = percent;
3133
+ if (Math.abs(velocity) > 0.3 && edgeStretchV === 0) {
3134
+ s2.targetOriginPercent = velocity > 0 ? 0 : 100;
3135
+ }
3136
+ s2.stretchOriginPercent += (s2.targetOriginPercent - s2.stretchOriginPercent) * 0.15;
3137
+ const rawHorizontalOffset = e.clientX - s2.pointerStart.x;
3138
+ const rubberBandedOffset = rawHorizontalOffset * 0.06;
3139
+ s2.horizontalOffset = Math.max(-5, Math.min(5, rubberBandedOffset));
3140
+ const absVelocity = Math.abs(velocity);
3141
+ const velStretch = Math.min(absVelocity * config.velocityStretch * 15, 1.2);
3142
+ const edgeStretchFactorV = Math.min(Math.abs(edgeStretchV) * 0.6, 0.12);
3143
+ const edgeStretchFactorH = Math.abs(s2.horizontalOffset) / 5 * 0.06;
3144
+ const targetStretchY = 1 + velStretch + edgeStretchFactorV;
3145
+ const targetStretchX = 1 - velStretch * 0.4 + edgeStretchFactorH;
3146
+ const baseLerp = 0.35;
3147
+ const decayLerp = 0.15;
3148
+ const velocityThreshold = 0.5;
3149
+ if (absVelocity < velocityThreshold) {
3150
+ const decayFactor = 1 - absVelocity / velocityThreshold;
3151
+ s2.stretchY += (1 - s2.stretchY) * decayLerp * decayFactor;
3152
+ s2.stretchX += (1 - s2.stretchX) * decayLerp * decayFactor;
3153
+ s2.stretchOriginPercent += (50 - s2.stretchOriginPercent) * decayLerp * decayFactor;
3154
+ }
3155
+ s2.stretchY += (targetStretchY - s2.stretchY) * baseLerp;
3156
+ s2.stretchX += (targetStretchX - s2.stretchX) * baseLerp;
3157
+ const targetShadowY = 2 + absVelocity * 0.4;
3158
+ const targetShadowBlur = 6 + absVelocity * 0.6;
3159
+ const targetShadowOpacity = 0.12 + absVelocity * 0.015;
3160
+ s2.shadowY += (targetShadowY - s2.shadowY) * 0.3;
3161
+ s2.shadowBlur += (targetShadowBlur - s2.shadowBlur) * 0.3;
3162
+ s2.shadowOpacity += (targetShadowOpacity - s2.shadowOpacity) * 0.3;
3163
+ s2.displayPercent = Math.max(0, Math.min(100, percent));
3164
+ onChange(percentToValue(Math.max(0, Math.min(100, percent))));
3165
+ rerender();
3166
+ },
3167
+ [config.velocityStretch, percentToValue, onChange, rerender]
3168
+ );
3169
+ const handlePointerUp = useCallback(() => {
3170
+ const s2 = state.current;
3171
+ if (!s2.isDragging) return;
3172
+ s2.isDragging = false;
3173
+ s2.pointerStart = null;
3174
+ PhysicsEngine.unregister(decayIdRef.current);
3175
+ animateScaleTo(1);
3176
+ animateStretchRelax();
3177
+ if (s2.horizontalOffset !== 0) {
3178
+ PhysicsEngine.unregister(horizontalIdRef.current);
3179
+ horizontalSpringRef.current = new Spring1D(s2.horizontalOffset, {
3180
+ tension: 400,
3181
+ friction: 18,
3182
+ mass: 0.5,
3183
+ precision: 0.1
3184
+ });
3185
+ horizontalSpringRef.current.setTarget(0);
3186
+ PhysicsEngine.register(horizontalIdRef.current, (dt) => {
3187
+ if (!horizontalSpringRef.current) return;
3188
+ const result = horizontalSpringRef.current.step(dt);
3189
+ s2.horizontalOffset = result.value;
3190
+ rerender();
3191
+ if (result.isSettled) {
3192
+ s2.horizontalOffset = 0;
3193
+ PhysicsEngine.unregister(horizontalIdRef.current);
3194
+ }
3195
+ });
3196
+ }
3197
+ rerender();
3198
+ }, [animateScaleTo, animateStretchRelax, rerender]);
3199
+ const handleTrackClick = useCallback(
3200
+ (e) => {
3201
+ const s2 = state.current;
3202
+ if (!trackRef.current || s2.isDragging) return;
3203
+ const rect = trackRef.current.getBoundingClientRect();
3204
+ const y = e.clientY - rect.top;
3205
+ const percent = 100 - Math.max(0, Math.min(100, y / rect.height * 100));
3206
+ onChange(percentToValue(percent));
3207
+ animateToPercent(percent);
3208
+ },
3209
+ [percentToValue, onChange, animateToPercent]
3210
+ );
3211
+ useEffect(() => {
3212
+ return () => {
3213
+ PhysicsEngine.unregister(physicsIdRef.current);
3214
+ PhysicsEngine.unregister(scaleIdRef.current);
3215
+ PhysicsEngine.unregister(decayIdRef.current);
3216
+ PhysicsEngine.unregister(relaxIdRef.current);
3217
+ PhysicsEngine.unregister(horizontalIdRef.current);
3218
+ };
3219
+ }, []);
3220
+ const s = state.current;
3221
+ const displayValue = Math.round(value);
3222
+ return /* @__PURE__ */ jsxs("div", { className: `tekiyo-vertical-slider ${className}`, style: { height }, children: [
3223
+ /* @__PURE__ */ jsxs(
3224
+ "div",
3225
+ {
3226
+ className: "tekiyo-vertical-track",
3227
+ ref: trackRef,
3228
+ onClick: handleTrackClick,
3229
+ onPointerMove: handlePointerMove,
3230
+ onPointerUp: handlePointerUp,
3231
+ onPointerCancel: handlePointerUp,
3232
+ style: { height },
3233
+ children: [
3234
+ /* @__PURE__ */ jsx("div", { className: "tekiyo-vertical-track-bg" }),
3235
+ /* @__PURE__ */ jsx(
3236
+ "div",
3237
+ {
3238
+ className: "tekiyo-vertical-track-fill",
3239
+ style: {
3240
+ height: `${s.displayPercent}%`,
3241
+ background: color
3242
+ }
3243
+ }
3244
+ ),
3245
+ /* @__PURE__ */ jsx(
3246
+ "div",
3247
+ {
3248
+ className: `tekiyo-slider-thumb tekiyo-vertical-thumb ${s.isDragging ? "active" : ""}`,
3249
+ style: {
3250
+ bottom: `${s.displayPercent}%`,
3251
+ transform: `translate(calc(-50% + ${s.horizontalOffset}px), 50%) scale(${s.thumbScale}) scaleX(${s.stretchX}) scaleY(${s.stretchY})`,
3252
+ transformOrigin: `center ${s.stretchOriginPercent}%`,
3253
+ boxShadow: `0 ${s.shadowY}px ${s.shadowBlur}px rgba(0, 0, 0, ${s.shadowOpacity})`
3254
+ },
3255
+ onPointerDown: handlePointerDown
3256
+ }
3257
+ )
3258
+ ]
3259
+ }
3260
+ ),
3261
+ /* @__PURE__ */ jsxs("div", { className: "tekiyo-vertical-slider-info", children: [
3262
+ icon && /* @__PURE__ */ jsx("div", { className: "tekiyo-vertical-slider-icon", style: { color }, children: icon }),
3263
+ /* @__PURE__ */ jsxs("span", { className: "tekiyo-vertical-slider-value", style: { color }, children: [
3264
+ displayValue,
3265
+ "%"
3266
+ ] }),
3267
+ /* @__PURE__ */ jsx("span", { className: "tekiyo-vertical-slider-label", children: label })
3268
+ ] })
3269
+ ] });
3270
+ }
3271
+ const sliderPresets = [
3272
+ {
3273
+ name: "Buttery Smooth",
3274
+ description: "iOS classic feel",
3275
+ config: {
3276
+ springTension: 0.8,
3277
+ velocityStretch: 0.02,
3278
+ liftScale: 1.5,
3279
+ flickMomentum: 800,
3280
+ maxVelocity: 3
3281
+ }
3282
+ },
3283
+ {
3284
+ name: "Snappy",
3285
+ description: "Responsive & precise",
3286
+ config: {
3287
+ springTension: 1.4,
3288
+ velocityStretch: 0.01,
3289
+ liftScale: 1.05,
3290
+ flickMomentum: 400,
3291
+ maxVelocity: 5
3292
+ }
3293
+ },
3294
+ {
3295
+ name: "Playful",
3296
+ description: "Fun & elastic",
3297
+ config: {
3298
+ springTension: 0.5,
3299
+ velocityStretch: 0.08,
3300
+ liftScale: 1.5,
3301
+ flickMomentum: 1500,
3302
+ maxVelocity: 6
3303
+ }
3304
+ }
3305
+ ];
3306
+ const defaultSliderConfig = sliderPresets[0].config;
3307
+ function SegmentedControl({
3308
+ options,
3309
+ defaultIndex = 0,
3310
+ value,
3311
+ config,
3312
+ showIndicator,
3313
+ onChange,
3314
+ className = ""
3315
+ }) {
3316
+ const [, forceRender] = useState(0);
3317
+ const rerender = useCallback(() => forceRender((n) => n + 1), []);
3318
+ const initialIndex = value !== void 0 ? value : defaultIndex;
3319
+ const state = useRef({
3320
+ selected: initialIndex,
3321
+ indicatorPos: initialIndex,
3322
+ stretchX: 1,
3323
+ stretchY: 1,
3324
+ pressScale: 1,
3325
+ verticalOffset: 0,
3326
+ isDragging: false,
3327
+ isPressed: false,
3328
+ stretchOriginPercent: 50,
3329
+ targetOriginPercent: 50,
3330
+ velocity: 0,
3331
+ shadowY: 4,
3332
+ shadowBlur: 12,
3333
+ shadowOpacity: 0.15
3334
+ });
3335
+ const containerRef = useRef(null);
3336
+ const springRef = useRef(null);
3337
+ const pressSpringRef = useRef(null);
3338
+ const verticalSpringRef = useRef(null);
3339
+ const stretchOriginSpringRef = useRef(null);
3340
+ const physicsId = useRef(generatePhysicsId("seg"));
3341
+ const pressId = useRef(generatePhysicsId("press"));
3342
+ const verticalId = useRef(generatePhysicsId("vert"));
3343
+ const stretchOriginId = useRef(generatePhysicsId("segOrigin"));
3344
+ const pointerStart = useRef(null);
3345
+ const lastPos = useRef(initialIndex);
3346
+ const hasDragged = useRef(false);
3347
+ const velocitySamples = useRef([]);
3348
+ useEffect(() => {
3349
+ if (value !== void 0 && value !== state.current.selected && !state.current.isDragging) {
3350
+ animateTo(value);
3351
+ }
3352
+ }, [value]);
3353
+ const getIndexFromX = useCallback((clientX) => {
3354
+ if (!containerRef.current) return state.current.selected;
3355
+ const rect = containerRef.current.getBoundingClientRect();
3356
+ const x = Math.max(0, Math.min(rect.width, clientX - rect.left));
3357
+ return Math.min(options.length - 1, Math.floor(x / rect.width * options.length));
3358
+ }, [options.length]);
3359
+ const getPosFromX = useCallback((clientX, allowOverdrag = false) => {
3360
+ if (!containerRef.current) return state.current.indicatorPos;
3361
+ const rect = containerRef.current.getBoundingClientRect();
3362
+ const x = clientX - rect.left;
3363
+ const rel = allowOverdrag ? x / rect.width : Math.max(0, Math.min(1, x / rect.width));
3364
+ return rel * (options.length - 1);
3365
+ }, [options.length]);
3366
+ const animateTo = useCallback((target, initialVelocity = 0) => {
3367
+ const s2 = state.current;
3368
+ s2.targetOriginPercent = target > s2.indicatorPos ? 0 : 100;
3369
+ s2.selected = target;
3370
+ PhysicsEngine.unregister(physicsId.current);
3371
+ PhysicsEngine.unregister(stretchOriginId.current);
3372
+ if (s2.pressScale !== 1) {
3373
+ PhysicsEngine.unregister(pressId.current);
3374
+ const releaseSpring = new Spring1D(s2.pressScale, {
3375
+ tension: 200,
3376
+ friction: 18,
3377
+ mass: 0.8,
3378
+ precision: 0.01
3379
+ });
3380
+ releaseSpring.setTarget(1);
3381
+ PhysicsEngine.register(pressId.current, (dt2) => {
3382
+ const pr = releaseSpring.step(dt2);
3383
+ s2.pressScale = pr.value;
3384
+ rerender();
3385
+ if (pr.isSettled) {
3386
+ s2.pressScale = 1;
3387
+ PhysicsEngine.unregister(pressId.current);
3388
+ }
3389
+ });
3390
+ }
3391
+ springRef.current = new Spring1D(s2.indicatorPos, {
3392
+ tension: config.springTension * 280,
3393
+ friction: 18,
3394
+ mass: 0.8,
3395
+ precision: 1e-3
3396
+ });
3397
+ springRef.current.setTarget(target);
3398
+ if (initialVelocity) springRef.current.setVelocity(initialVelocity);
3399
+ PhysicsEngine.register(physicsId.current, (dt) => {
3400
+ if (!springRef.current) return;
3401
+ const r = springRef.current.step(dt);
3402
+ const vel = Math.abs(r.velocity || 0);
3403
+ const stretch = Math.min(vel * config.velocityStretch * 6, 0.25);
3404
+ s2.indicatorPos = r.value;
3405
+ s2.velocity = vel;
3406
+ s2.stretchX = 1 + stretch;
3407
+ s2.stretchY = 1 - stretch * 0.5;
3408
+ s2.stretchOriginPercent += (s2.targetOriginPercent - s2.stretchOriginPercent) * 0.12;
3409
+ s2.shadowY = 4 + vel * 2;
3410
+ s2.shadowBlur = 12 + vel * 4;
3411
+ s2.shadowOpacity = 0.15 + vel * 0.05;
3412
+ rerender();
3413
+ if (r.isSettled) {
3414
+ s2.stretchX = 1;
3415
+ s2.stretchY = 1;
3416
+ s2.velocity = 0;
3417
+ s2.shadowY = 4;
3418
+ s2.shadowBlur = 12;
3419
+ s2.shadowOpacity = 0.15;
3420
+ PhysicsEngine.unregister(physicsId.current);
3421
+ stretchOriginSpringRef.current = new Spring1D(s2.stretchOriginPercent, {
3422
+ tension: 300,
3423
+ friction: 18,
3424
+ mass: 0.5,
3425
+ precision: 0.1
3426
+ });
3427
+ stretchOriginSpringRef.current.setTarget(50);
3428
+ PhysicsEngine.register(stretchOriginId.current, (dt2) => {
3429
+ if (!stretchOriginSpringRef.current) return;
3430
+ const or = stretchOriginSpringRef.current.step(dt2);
3431
+ s2.stretchOriginPercent = or.value;
3432
+ s2.targetOriginPercent = 50;
3433
+ rerender();
3434
+ if (or.isSettled) {
3435
+ s2.stretchOriginPercent = 50;
3436
+ s2.targetOriginPercent = 50;
3437
+ PhysicsEngine.unregister(stretchOriginId.current);
3438
+ }
3439
+ });
3440
+ rerender();
3441
+ }
3442
+ });
3443
+ onChange == null ? void 0 : onChange(target);
3444
+ }, [config.springTension, config.velocityStretch, onChange, rerender]);
3445
+ const animatePress = useCallback((pressed) => {
3446
+ const s2 = state.current;
3447
+ PhysicsEngine.unregister(pressId.current);
3448
+ pressSpringRef.current = new Spring1D(s2.pressScale, {
3449
+ tension: 400,
3450
+ friction: 22,
3451
+ mass: 0.6,
3452
+ precision: 0.01
3453
+ });
3454
+ pressSpringRef.current.setTarget(pressed ? 1.4 : 1);
3455
+ PhysicsEngine.register(pressId.current, (dt) => {
3456
+ if (!pressSpringRef.current) return;
3457
+ const r = pressSpringRef.current.step(dt);
3458
+ s2.pressScale = r.value;
3459
+ rerender();
3460
+ if (r.isSettled) PhysicsEngine.unregister(pressId.current);
3461
+ });
3462
+ }, [rerender]);
3463
+ const animateVerticalBack = useCallback(() => {
3464
+ const s2 = state.current;
3465
+ if (Math.abs(s2.verticalOffset) < 0.5) {
3466
+ s2.verticalOffset = 0;
3467
+ return;
3468
+ }
3469
+ PhysicsEngine.unregister(verticalId.current);
3470
+ verticalSpringRef.current = new Spring1D(s2.verticalOffset, {
3471
+ tension: 400,
3472
+ friction: 20,
3473
+ mass: 0.5,
3474
+ precision: 0.1
3475
+ });
3476
+ verticalSpringRef.current.setTarget(0);
3477
+ PhysicsEngine.register(verticalId.current, (dt) => {
3478
+ if (!verticalSpringRef.current) return;
3479
+ const r = verticalSpringRef.current.step(dt);
3480
+ s2.verticalOffset = r.value;
3481
+ rerender();
3482
+ if (r.isSettled) {
3483
+ s2.verticalOffset = 0;
3484
+ PhysicsEngine.unregister(verticalId.current);
3485
+ }
3486
+ });
3487
+ }, [rerender]);
3488
+ const animateOriginBack = useCallback(() => {
3489
+ const s2 = state.current;
3490
+ if (Math.abs(s2.stretchOriginPercent - 50) < 1) {
3491
+ s2.stretchOriginPercent = 50;
3492
+ s2.targetOriginPercent = 50;
3493
+ return;
3494
+ }
3495
+ PhysicsEngine.unregister(stretchOriginId.current);
3496
+ stretchOriginSpringRef.current = new Spring1D(s2.stretchOriginPercent, {
3497
+ tension: 300,
3498
+ friction: 18,
3499
+ mass: 0.5,
3500
+ precision: 0.1
3501
+ });
3502
+ stretchOriginSpringRef.current.setTarget(50);
3503
+ PhysicsEngine.register(stretchOriginId.current, (dt) => {
3504
+ if (!stretchOriginSpringRef.current) return;
3505
+ const r = stretchOriginSpringRef.current.step(dt);
3506
+ s2.stretchOriginPercent = r.value;
3507
+ s2.targetOriginPercent = 50;
3508
+ rerender();
3509
+ if (r.isSettled) {
3510
+ s2.stretchOriginPercent = 50;
3511
+ s2.targetOriginPercent = 50;
3512
+ PhysicsEngine.unregister(stretchOriginId.current);
3513
+ }
3514
+ });
3515
+ }, [rerender]);
3516
+ const onPointerDown = useCallback((e) => {
3517
+ const s2 = state.current;
3518
+ pointerStart.current = { x: e.clientX, y: e.clientY };
3519
+ hasDragged.current = false;
3520
+ velocitySamples.current = [];
3521
+ e.currentTarget.setPointerCapture(e.pointerId);
3522
+ PhysicsEngine.unregister(stretchOriginId.current);
3523
+ if (containerRef.current) {
3524
+ const rect = containerRef.current.getBoundingClientRect();
3525
+ const segW = rect.width / options.length;
3526
+ const indLeft = rect.left + s2.indicatorPos * segW;
3527
+ const grabX = e.clientX - indLeft;
3528
+ const pct = Math.max(0, Math.min(100, grabX / segW * 100));
3529
+ s2.targetOriginPercent = pct;
3530
+ s2.stretchOriginPercent = pct;
3531
+ }
3532
+ s2.isPressed = true;
3533
+ animatePress(true);
3534
+ lastPos.current = getPosFromX(e.clientX);
3535
+ rerender();
3536
+ }, [options.length, getPosFromX, animatePress, rerender]);
3537
+ const onPointerMove = useCallback((e) => {
3538
+ if (!pointerStart.current) return;
3539
+ const s2 = state.current;
3540
+ const dx = e.clientX - pointerStart.current.x;
3541
+ const dy = e.clientY - pointerStart.current.y;
3542
+ if (!hasDragged.current && (Math.abs(dx) > 5 || Math.abs(dy) > 5)) {
3543
+ hasDragged.current = true;
3544
+ s2.isDragging = true;
3545
+ PhysicsEngine.unregister(physicsId.current);
3546
+ }
3547
+ if (!hasDragged.current) return;
3548
+ let pos = getPosFromX(e.clientX, true);
3549
+ const minPos = 0;
3550
+ const maxPos = options.length - 1;
3551
+ let edgeStretch = 0;
3552
+ if (pos < minPos) {
3553
+ const over = minPos - pos;
3554
+ edgeStretch = -over;
3555
+ pos = minPos - over * 0.15;
3556
+ s2.targetOriginPercent = 100;
3557
+ } else if (pos > maxPos) {
3558
+ const over = pos - maxPos;
3559
+ edgeStretch = over;
3560
+ pos = maxPos + over * 0.15;
3561
+ s2.targetOriginPercent = 0;
3562
+ }
3563
+ const vel = pos - lastPos.current;
3564
+ if (Math.abs(vel) > 0.01 && edgeStretch === 0) {
3565
+ s2.targetOriginPercent = vel > 0 ? 100 : 0;
3566
+ }
3567
+ s2.stretchOriginPercent += (s2.targetOriginPercent - s2.stretchOriginPercent) * 0.15;
3568
+ velocitySamples.current.push(vel);
3569
+ if (velocitySamples.current.length > 5) velocitySamples.current.shift();
3570
+ lastPos.current = pos;
3571
+ const vOff = Math.max(-12, Math.min(12, dy * 0.12));
3572
+ s2.verticalOffset = vOff;
3573
+ const vFactor = Math.min(Math.abs(vel) * config.velocityStretch * 80, 0.6);
3574
+ const edgeFactor = Math.min(Math.abs(edgeStretch) * 0.2, 0.2);
3575
+ s2.stretchX = 1 + vFactor + edgeFactor;
3576
+ s2.stretchY = 1 - vFactor * 0.5 - edgeFactor * 0.4;
3577
+ s2.indicatorPos = pos;
3578
+ const idx = getIndexFromX(e.clientX);
3579
+ if (idx !== s2.selected) s2.selected = idx;
3580
+ rerender();
3581
+ }, [getPosFromX, getIndexFromX, options.length, config.velocityStretch, rerender]);
3582
+ const onPointerUp = useCallback((e) => {
3583
+ const s2 = state.current;
3584
+ pointerStart.current = null;
3585
+ s2.isPressed = false;
3586
+ if (!hasDragged.current) {
3587
+ const idx = getIndexFromX(e.clientX);
3588
+ if (idx !== s2.selected) {
3589
+ const dir = idx > s2.indicatorPos ? 1 : -1;
3590
+ animateTo(idx, dir * 3);
3591
+ } else {
3592
+ animatePress(false);
3593
+ animateOriginBack();
3594
+ }
3595
+ rerender();
3596
+ return;
3597
+ }
3598
+ s2.isDragging = false;
3599
+ hasDragged.current = false;
3600
+ animatePress(false);
3601
+ const avgVel = velocitySamples.current.length > 0 ? velocitySamples.current.reduce((a, b) => a + b, 0) / velocitySamples.current.length : 0;
3602
+ velocitySamples.current = [];
3603
+ animateVerticalBack();
3604
+ animateTo(s2.selected, avgVel * 0.5);
3605
+ }, [getIndexFromX, animateTo, animatePress, animateVerticalBack, animateOriginBack, rerender]);
3606
+ const onPointerLeave = useCallback(() => {
3607
+ if (pointerStart.current) {
3608
+ const s2 = state.current;
3609
+ pointerStart.current = null;
3610
+ s2.isPressed = false;
3611
+ s2.isDragging = false;
3612
+ hasDragged.current = false;
3613
+ velocitySamples.current = [];
3614
+ animatePress(false);
3615
+ animateVerticalBack();
3616
+ animateTo(s2.selected);
3617
+ }
3618
+ }, [animatePress, animateVerticalBack, animateTo]);
3619
+ useEffect(() => {
3620
+ return () => {
3621
+ PhysicsEngine.unregister(physicsId.current);
3622
+ PhysicsEngine.unregister(pressId.current);
3623
+ PhysicsEngine.unregister(verticalId.current);
3624
+ PhysicsEngine.unregister(stretchOriginId.current);
3625
+ };
3626
+ }, []);
3627
+ const s = state.current;
3628
+ const dragClass = s.isPressed || s.isDragging ? "dragging" : "";
3629
+ return /* @__PURE__ */ jsxs(
3630
+ "div",
3631
+ {
3632
+ ref: containerRef,
3633
+ className: `tekiyo-segmented-control ${s.isDragging ? "dragging" : ""} ${className}`,
3634
+ onPointerDown,
3635
+ onPointerMove,
3636
+ onPointerUp,
3637
+ onPointerCancel: onPointerUp,
3638
+ onPointerLeave,
3639
+ children: [
3640
+ /* @__PURE__ */ jsx(
3641
+ "div",
3642
+ {
3643
+ className: `tekiyo-segmented-indicator ${s.isPressed ? "active" : ""} ${dragClass}`,
3644
+ style: {
3645
+ width: `calc((100% - 6px) / ${options.length})`,
3646
+ left: `calc(${s.indicatorPos} * (100% - 6px) / ${options.length} + 3px)`,
3647
+ transform: `translateY(${s.verticalOffset}px) scaleX(${s.stretchX * s.pressScale}) scaleY(${s.stretchY * s.pressScale})`,
3648
+ transformOrigin: `${s.stretchOriginPercent}% center`
3649
+ }
3650
+ }
3651
+ ),
3652
+ options.map((option, index) => /* @__PURE__ */ jsxs(
3653
+ "div",
3654
+ {
3655
+ className: `tekiyo-segmented-option ${s.selected === index ? "active" : ""} ${dragClass}`,
3656
+ children: [
3657
+ option,
3658
+ showIndicator && s.selected === index && /* @__PURE__ */ jsx("span", { className: `tekiyo-option-dot ${dragClass}` })
3659
+ ]
3660
+ },
3661
+ option
3662
+ ))
3663
+ ]
3664
+ }
3665
+ );
3666
+ }
3667
+ const segmentedControlPresets = [
3668
+ {
3669
+ name: "Buttery Smooth",
3670
+ description: "iOS classic feel",
3671
+ config: {
3672
+ springTension: 0.8,
3673
+ velocityStretch: 0.02,
3674
+ liftScale: 1.5,
3675
+ flickMomentum: 800,
3676
+ maxVelocity: 3
3677
+ }
3678
+ },
3679
+ {
3680
+ name: "Snappy",
3681
+ description: "Responsive & precise",
3682
+ config: {
3683
+ springTension: 1.4,
3684
+ velocityStretch: 0.01,
3685
+ liftScale: 1.05,
3686
+ flickMomentum: 400,
3687
+ maxVelocity: 5
3688
+ }
3689
+ },
3690
+ {
3691
+ name: "Playful",
3692
+ description: "Fun & elastic",
3693
+ config: {
3694
+ springTension: 0.5,
3695
+ velocityStretch: 0.08,
3696
+ liftScale: 1.5,
3697
+ flickMomentum: 1500,
3698
+ maxVelocity: 6
3699
+ }
3700
+ }
3701
+ ];
3702
+ const defaultSegmentedControlConfig = segmentedControlPresets[0].config;
3703
+ const DEFAULT_LIQUID_GLASS_CONFIG = {
3704
+ width: 200,
3705
+ height: 60,
3706
+ bezelWidth: 20,
3707
+ glassThickness: 0.5,
3708
+ refractiveIndex: 1.5,
3709
+ profile: "squircle",
3710
+ scale: 70
3711
+ };
3712
+ const mapCache = /* @__PURE__ */ new Map();
3713
+ function getCacheKey(config) {
3714
+ return `${config.width}-${config.height}-${config.bezelWidth}-${config.glassThickness}-${config.refractiveIndex}-${config.profile}`;
3715
+ }
3716
+ function preloadMaps(width, height, bezelWidth = 20, glassThickness = 0.5, refractiveIndex = 1.5) {
3717
+ const profiles = ["convex", "squircle", "concave", "lip"];
3718
+ profiles.forEach((profile) => {
3719
+ const config = {
3720
+ width,
3721
+ height,
3722
+ bezelWidth,
3723
+ glassThickness,
3724
+ refractiveIndex,
3725
+ profile
3726
+ };
3727
+ const key = getCacheKey(config);
3728
+ if (!mapCache.has(key)) {
3729
+ mapCache.set(key, {
3730
+ displacement: generateDisplacementMapUncached(config),
3731
+ specular: generateSpecularMapUncached(config)
3732
+ });
3733
+ }
3734
+ });
3735
+ }
3736
+ function getCachedMaps(config) {
3737
+ const key = getCacheKey(config);
3738
+ if (mapCache.has(key)) {
3739
+ return mapCache.get(key);
3740
+ }
3741
+ const maps = {
3742
+ displacement: generateDisplacementMapUncached(config),
3743
+ specular: generateSpecularMapUncached(config)
3744
+ };
3745
+ mapCache.set(key, maps);
3746
+ return maps;
3747
+ }
3748
+ function clearMapCache() {
3749
+ mapCache.clear();
3750
+ }
3751
+ function smootherstep(x) {
3752
+ const t = Math.max(0, Math.min(1, x));
3753
+ return t * t * t * (t * (t * 6 - 15) + 10);
3754
+ }
3755
+ function getProfileFunction(profile) {
3756
+ switch (profile) {
3757
+ case "convex":
3758
+ return (x) => Math.sqrt(Math.max(0, 1 - Math.pow(1 - x, 2)));
3759
+ case "squircle":
3760
+ return (x) => Math.pow(Math.max(0, 1 - Math.pow(1 - x, 4)), 0.25);
3761
+ case "concave":
3762
+ return (x) => 1 - Math.sqrt(Math.max(0, 1 - Math.pow(1 - x, 2)));
3763
+ case "lip":
3764
+ return (x) => {
3765
+ const convex = Math.sqrt(Math.max(0, 1 - Math.pow(1 - x, 2)));
3766
+ const concave = 1 - convex;
3767
+ const t = smootherstep(x);
3768
+ return convex * (1 - t) + concave * t;
3769
+ };
3770
+ default:
3771
+ return (x) => x;
3772
+ }
3773
+ }
3774
+ function calculateNormal(f, x) {
3775
+ const delta = 1e-3;
3776
+ const x1 = Math.max(0, x - delta);
3777
+ const x2 = Math.min(1, x + delta);
3778
+ const y1 = f(x1);
3779
+ const y2 = f(x2);
3780
+ const derivative = (y2 - y1) / (x2 - x1);
3781
+ const length = Math.sqrt(derivative * derivative + 1);
3782
+ return {
3783
+ nx: -derivative / length,
3784
+ ny: 1 / length
3785
+ };
3786
+ }
3787
+ function applySnellLaw(incidentAngle, n1, n2) {
3788
+ const sinTheta1 = Math.sin(incidentAngle);
3789
+ const sinTheta2 = n1 / n2 * sinTheta1;
3790
+ if (Math.abs(sinTheta2) > 1) {
3791
+ return incidentAngle;
3792
+ }
3793
+ return Math.asin(sinTheta2);
3794
+ }
3795
+ function calculateDisplacement(normalizedDist, angle, profile, glassThickness, refractiveIndex) {
3796
+ if (normalizedDist >= 1) {
3797
+ return { dx: 0, dy: 0 };
3798
+ }
3799
+ const height = profile(normalizedDist) * glassThickness;
3800
+ const normal = calculateNormal(profile, normalizedDist);
3801
+ const incidentAngle = Math.atan2(normal.nx, normal.ny);
3802
+ const refractedAngle = applySnellLaw(incidentAngle, 1, refractiveIndex);
3803
+ const angleDiff = refractedAngle - incidentAngle;
3804
+ const magnitude = Math.sin(angleDiff) * height;
3805
+ const dx = magnitude * Math.cos(angle);
3806
+ const dy = magnitude * Math.sin(angle);
3807
+ return { dx, dy };
3808
+ }
3809
+ function vectorToRGB(dx, dy) {
3810
+ return {
3811
+ r: Math.round(128 + dx * 127),
3812
+ g: Math.round(128 + dy * 127),
3813
+ b: 128
3814
+ // Unused channel
3815
+ };
3816
+ }
3817
+ function distanceToRoundedRectEdge(x, y, width, height, cornerRadius) {
3818
+ const halfW = width / 2;
3819
+ const halfH = height / 2;
3820
+ const cx = x - halfW;
3821
+ const cy = y - halfH;
3822
+ const r = Math.min(cornerRadius, halfW, halfH);
3823
+ const absX = Math.abs(cx);
3824
+ const absY = Math.abs(cy);
3825
+ const cornerX = halfW - r;
3826
+ const cornerY = halfH - r;
3827
+ if (absX > cornerX && absY > cornerY) {
3828
+ const cornerCenterX = cornerX * Math.sign(cx);
3829
+ const cornerCenterY = cornerY * Math.sign(cy);
3830
+ const dx = cx - cornerCenterX;
3831
+ const dy = cy - cornerCenterY;
3832
+ const distFromCorner = Math.sqrt(dx * dx + dy * dy);
3833
+ const distToEdge = r - distFromCorner;
3834
+ const angle = Math.atan2(dy, dx);
3835
+ return { distance: Math.max(0, distToEdge), angle };
3836
+ }
3837
+ if (absX > absY * (halfW / halfH)) {
3838
+ const distToEdge = halfW - absX;
3839
+ const angle = cx > 0 ? 0 : Math.PI;
3840
+ return { distance: Math.max(0, distToEdge), angle };
3841
+ } else {
3842
+ const distToEdge = halfH - absY;
3843
+ const angle = cy > 0 ? Math.PI / 2 : -Math.PI / 2;
3844
+ return { distance: Math.max(0, distToEdge), angle };
3845
+ }
3846
+ }
3847
+ function generateDisplacementMapUncached(config) {
3848
+ const { width, height, bezelWidth, glassThickness, refractiveIndex, profile } = config;
3849
+ const canvas = document.createElement("canvas");
3850
+ canvas.width = width;
3851
+ canvas.height = height;
3852
+ const ctx = canvas.getContext("2d");
3853
+ if (!ctx) {
3854
+ console.error("Failed to get canvas context");
3855
+ return "";
3856
+ }
3857
+ const profileFn = getProfileFunction(profile);
3858
+ const imageData = ctx.createImageData(width, height);
3859
+ const data = imageData.data;
3860
+ const cornerRadius = Math.min(width, height) / 2;
3861
+ for (let y = 0; y < height; y++) {
3862
+ for (let x = 0; x < width; x++) {
3863
+ const { distance, angle } = distanceToRoundedRectEdge(
3864
+ x,
3865
+ y,
3866
+ width,
3867
+ height,
3868
+ cornerRadius
3869
+ );
3870
+ const normalizedDist = Math.min(1, distance / bezelWidth);
3871
+ const { dx, dy } = calculateDisplacement(
3872
+ normalizedDist,
3873
+ angle,
3874
+ profileFn,
3875
+ glassThickness,
3876
+ refractiveIndex
3877
+ );
3878
+ const { r, g, b } = vectorToRGB(dx, dy);
3879
+ const idx = (y * width + x) * 4;
3880
+ data[idx] = r;
3881
+ data[idx + 1] = g;
3882
+ data[idx + 2] = b;
3883
+ data[idx + 3] = 255;
3884
+ }
3885
+ }
3886
+ ctx.putImageData(imageData, 0, 0);
3887
+ return canvas.toDataURL("image/png");
3888
+ }
3889
+ function generateSpecularMapUncached(config) {
3890
+ const { width, height, bezelWidth, glassThickness, profile } = config;
3891
+ const canvas = document.createElement("canvas");
3892
+ canvas.width = width;
3893
+ canvas.height = height;
3894
+ const ctx = canvas.getContext("2d");
3895
+ if (!ctx) {
3896
+ console.error("Failed to get canvas context");
3897
+ return "";
3898
+ }
3899
+ const profileFn = getProfileFunction(profile);
3900
+ const imageData = ctx.createImageData(width, height);
3901
+ const data = imageData.data;
3902
+ const cornerRadius = Math.min(width, height) / 2;
3903
+ for (let y = 0; y < height; y++) {
3904
+ for (let x = 0; x < width; x++) {
3905
+ const { distance } = distanceToRoundedRectEdge(
3906
+ x,
3907
+ y,
3908
+ width,
3909
+ height,
3910
+ cornerRadius
3911
+ );
3912
+ const normalizedDist = Math.min(1, distance / bezelWidth);
3913
+ let specularIntensity = 0;
3914
+ if (normalizedDist < 1) {
3915
+ const delta = 0.02;
3916
+ const h1 = profileFn(Math.max(0, normalizedDist - delta));
3917
+ const h2 = profileFn(Math.min(1, normalizedDist + delta));
3918
+ const gradient = Math.abs((h2 - h1) / (2 * delta));
3919
+ specularIntensity = Math.pow(gradient, 0.7) * glassThickness * 0.6;
3920
+ const rimStart = 0.25;
3921
+ if (normalizedDist < rimStart) {
3922
+ const rimT = 1 - normalizedDist / rimStart;
3923
+ const smoothRim = rimT * rimT * (3 - 2 * rimT);
3924
+ specularIntensity += smoothRim * 0.3;
3925
+ }
3926
+ specularIntensity = Math.min(1, specularIntensity);
3927
+ }
3928
+ const value = Math.round(specularIntensity * 255);
3929
+ const idx = (y * width + x) * 4;
3930
+ data[idx] = value;
3931
+ data[idx + 1] = value;
3932
+ data[idx + 2] = value;
3933
+ data[idx + 3] = 255;
3934
+ }
3935
+ }
3936
+ ctx.putImageData(imageData, 0, 0);
3937
+ return canvas.toDataURL("image/png");
3938
+ }
3939
+ function generateDisplacementMap(config) {
3940
+ return getCachedMaps(config).displacement;
3941
+ }
3942
+ function generateSpecularMap(config) {
3943
+ return getCachedMaps(config).specular;
3944
+ }
3945
+ function supportsBackdropSvgFilter() {
3946
+ if (typeof window === "undefined" || typeof navigator === "undefined") {
3947
+ return false;
3948
+ }
3949
+ const ua = navigator.userAgent;
3950
+ const isSafari = /Safari\//.test(ua) && !/Chrome\//.test(ua);
3951
+ const isFirefox = /Firefox\//.test(ua);
3952
+ const isChrome = /Chrome\//.test(ua) && !isSafari && !isFirefox;
3953
+ return isChrome;
3954
+ }
3955
+ const LiquidGlassFilter = ({
3956
+ filterId,
3957
+ displacementMapUrl,
3958
+ specularMapUrl,
3959
+ width,
3960
+ height,
3961
+ scale,
3962
+ saturation = 1.2,
3963
+ blurAmount = 1,
3964
+ specularIntensity = 0.25
3965
+ }) => {
3966
+ const filterSvg = useMemo(
3967
+ () => /* @__PURE__ */ jsx(
3968
+ "svg",
3969
+ {
3970
+ style: {
3971
+ position: "absolute",
3972
+ width: 0,
3973
+ height: 0,
3974
+ overflow: "hidden",
3975
+ pointerEvents: "none"
3976
+ },
3977
+ "aria-hidden": "true",
3978
+ children: /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsxs(
3979
+ "filter",
3980
+ {
3981
+ id: filterId,
3982
+ x: "-50%",
3983
+ y: "-50%",
3984
+ width: "200%",
3985
+ height: "200%",
3986
+ colorInterpolationFilters: "sRGB",
3987
+ children: [
3988
+ /* @__PURE__ */ jsx("feGaussianBlur", { in: "SourceGraphic", stdDeviation: blurAmount, result: "blurred" }),
3989
+ /* @__PURE__ */ jsx(
3990
+ "feImage",
3991
+ {
3992
+ href: displacementMapUrl,
3993
+ x: "0",
3994
+ y: "0",
3995
+ width,
3996
+ height,
3997
+ result: "displacement_map",
3998
+ preserveAspectRatio: "none"
3999
+ }
4000
+ ),
4001
+ /* @__PURE__ */ jsx(
4002
+ "feDisplacementMap",
4003
+ {
4004
+ in: "blurred",
4005
+ in2: "displacement_map",
4006
+ scale,
4007
+ xChannelSelector: "R",
4008
+ yChannelSelector: "G",
4009
+ result: "displaced"
4010
+ }
4011
+ ),
4012
+ /* @__PURE__ */ jsx(
4013
+ "feColorMatrix",
4014
+ {
4015
+ in: "displaced",
4016
+ type: "saturate",
4017
+ values: String(saturation),
4018
+ result: "saturated"
4019
+ }
4020
+ ),
4021
+ /* @__PURE__ */ jsx(
4022
+ "feImage",
4023
+ {
4024
+ href: specularMapUrl,
4025
+ x: "0",
4026
+ y: "0",
4027
+ width,
4028
+ height,
4029
+ result: "specular_map_raw",
4030
+ preserveAspectRatio: "none"
4031
+ }
4032
+ ),
4033
+ /* @__PURE__ */ jsx("feGaussianBlur", { in: "specular_map_raw", stdDeviation: "3", result: "specular_map" }),
4034
+ /* @__PURE__ */ jsx("feFlood", { floodColor: "white", floodOpacity: specularIntensity * 0.5, result: "white" }),
4035
+ /* @__PURE__ */ jsx("feComposite", { in: "white", in2: "specular_map", operator: "in", result: "highlight" }),
4036
+ /* @__PURE__ */ jsx("feBlend", { in: "highlight", in2: "saturated", mode: "screen", result: "final" })
4037
+ ]
4038
+ }
4039
+ ) })
4040
+ }
4041
+ ),
4042
+ [filterId, displacementMapUrl, specularMapUrl, width, height, scale, saturation, blurAmount, specularIntensity]
4043
+ );
4044
+ return filterSvg;
4045
+ };
4046
+ function generateFilterId(prefix = "liquid-glass") {
4047
+ return `${prefix}-${Math.random().toString(36).substr(2, 9)}`;
4048
+ }
4049
+ function useLiquidGlass(options) {
4050
+ const {
4051
+ width,
4052
+ height,
4053
+ profile = DEFAULT_LIQUID_GLASS_CONFIG.profile,
4054
+ bezelWidth = DEFAULT_LIQUID_GLASS_CONFIG.bezelWidth,
4055
+ glassThickness = DEFAULT_LIQUID_GLASS_CONFIG.glassThickness,
4056
+ refractiveIndex = DEFAULT_LIQUID_GLASS_CONFIG.refractiveIndex,
4057
+ animated = true,
4058
+ saturation = 1.3,
4059
+ blurAmount = 1,
4060
+ specularIntensity = 0.3
4061
+ } = options;
4062
+ const [scale, setScaleState] = useState(DEFAULT_LIQUID_GLASS_CONFIG.scale);
4063
+ const [displacementMapUrl, setDisplacementMapUrl] = useState("");
4064
+ const [specularMapUrl, setSpecularMapUrl] = useState("");
4065
+ const scaleSpringRef = useRef(null);
4066
+ const physicsIdRef = useRef(generatePhysicsId("liquid-glass"));
4067
+ const filterIdRef = useRef(generateFilterId());
4068
+ const isSupported = useMemo(() => supportsBackdropSvgFilter(), []);
4069
+ useEffect(() => {
4070
+ const config = {
4071
+ width,
4072
+ height,
4073
+ bezelWidth,
4074
+ glassThickness,
4075
+ refractiveIndex,
4076
+ profile
4077
+ };
4078
+ const dispMapUrl = generateDisplacementMap(config);
4079
+ const specMapUrl = generateSpecularMap(config);
4080
+ setDisplacementMapUrl(dispMapUrl);
4081
+ setSpecularMapUrl(specMapUrl);
4082
+ }, [width, height, bezelWidth, glassThickness, refractiveIndex, profile]);
4083
+ useEffect(() => {
4084
+ scaleSpringRef.current = new Spring1D(scale, {
4085
+ tension: 200,
4086
+ friction: 20,
4087
+ mass: 1,
4088
+ precision: 0.5
4089
+ });
4090
+ return () => {
4091
+ PhysicsEngine.unregister(physicsIdRef.current);
4092
+ };
4093
+ }, []);
4094
+ const setScale = useCallback(
4095
+ (targetScale) => {
4096
+ if (!animated) {
4097
+ setScaleState(targetScale);
4098
+ return;
4099
+ }
4100
+ if (!scaleSpringRef.current) {
4101
+ scaleSpringRef.current = new Spring1D(scale, {
4102
+ tension: 200,
4103
+ friction: 20,
4104
+ mass: 1,
4105
+ precision: 0.5
4106
+ });
4107
+ }
4108
+ scaleSpringRef.current.setTarget(targetScale);
4109
+ if (!PhysicsEngine.has(physicsIdRef.current)) {
4110
+ PhysicsEngine.register(physicsIdRef.current, (dt) => {
4111
+ if (!scaleSpringRef.current) return;
4112
+ const result = scaleSpringRef.current.step(dt);
4113
+ setScaleState(result.value);
4114
+ if (result.isSettled) {
4115
+ PhysicsEngine.unregister(physicsIdRef.current);
4116
+ }
4117
+ });
4118
+ }
4119
+ },
4120
+ [animated, scale]
4121
+ );
4122
+ const filterComponent = useMemo(() => {
4123
+ if (!displacementMapUrl || !specularMapUrl || !isSupported) {
4124
+ return null;
4125
+ }
4126
+ return React.createElement(LiquidGlassFilter, {
4127
+ filterId: filterIdRef.current,
4128
+ displacementMapUrl,
4129
+ specularMapUrl,
4130
+ width,
4131
+ height,
4132
+ scale,
4133
+ saturation,
4134
+ blurAmount,
4135
+ specularIntensity
4136
+ });
4137
+ }, [displacementMapUrl, specularMapUrl, width, height, scale, isSupported, saturation, blurAmount, specularIntensity]);
4138
+ const style = useMemo(() => {
4139
+ if (!isSupported) {
4140
+ return {
4141
+ backdropFilter: "blur(12px) saturate(1.2)",
4142
+ WebkitBackdropFilter: "blur(12px) saturate(1.2)"
4143
+ };
4144
+ }
4145
+ return {
4146
+ backdropFilter: `url(#${filterIdRef.current})`,
4147
+ WebkitBackdropFilter: `url(#${filterIdRef.current})`
4148
+ };
4149
+ }, [isSupported]);
4150
+ return {
4151
+ filterComponent,
4152
+ style,
4153
+ setScale,
4154
+ scale,
4155
+ isSupported
4156
+ };
4157
+ }
1876
4158
  export {
1877
4159
  Card,
1878
4160
  DEFAULT_FRICTION_CONFIG,
4161
+ DEFAULT_LIQUID_GLASS_CONFIG,
1879
4162
  DEFAULT_SPRING_CONFIG,
1880
4163
  Draggable,
4164
+ LiquidGlassFilter,
1881
4165
  Momentum,
1882
4166
  Momentum1D,
1883
4167
  PhysicsEngine,
1884
4168
  PhysicsProvider,
4169
+ PhysicsRangeSlider,
4170
+ PhysicsSlider,
4171
+ PhysicsStepSlider,
4172
+ PhysicsVerticalSlider,
4173
+ SegmentedControl,
1885
4174
  Spring,
1886
4175
  Spring1D,
1887
4176
  Vector2,
1888
4177
  VelocityTracker,
1889
4178
  VelocityTracker1D,
1890
4179
  cardBaseStyles,
4180
+ clearMapCache,
1891
4181
  combineLiftStyle,
1892
4182
  combineStretchStyle,
4183
+ defaultSegmentedControlConfig,
4184
+ defaultSliderConfig,
4185
+ generateDisplacementMap,
4186
+ generateFilterId,
1893
4187
  generatePhysicsId,
4188
+ generateSpecularMap,
1894
4189
  gentle,
4190
+ getCachedMaps,
1895
4191
  getFrictionConfig,
1896
4192
  getPreset,
4193
+ getProfileFunction,
1897
4194
  getSpringConfig,
1898
4195
  ios,
4196
+ preloadMaps,
1899
4197
  presets,
4198
+ segmentedControlPresets,
4199
+ sliderPresets,
1900
4200
  smooth,
4201
+ smootherstep,
1901
4202
  snappy,
1902
4203
  stiff,
4204
+ supportsBackdropSvgFilter,
1903
4205
  useDrag,
1904
4206
  useFlick,
1905
4207
  useFrictionConfig,
1906
4208
  useGesture,
1907
4209
  useLift,
1908
4210
  useLiftConfig,
4211
+ useLiquidGlass,
1909
4212
  usePhysicsConfig,
1910
4213
  usePhysicsContext,
1911
4214
  useSpring,