{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "slider",
  "title": "Slider",
  "description": "shadcn/ui(new-york-v4) slider의 유니버설(웹+네이티브 한 벌 소스) 포팅. 제거하고 브리지 useDrag(웹 PointerEvent·네이티브 PanResponder 동일 계약)로 직접 구현한다.",
  "dependencies": [
    "@unikit-bridge/ui"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/ui/slider.tsx",
      "content": "// Slider — shadcn/ui（new-york-v4） slider.tsx 유니버설 포팅 （사이클 40, T-2b）.\n// 원본: shadcn-ref/ui/slider.tsx — radix Slider 엔진（Root/Track/Range/Thumb）을\n// 제거하고 브리지 useDrag（웹 PointerEvent네이티브 PanResponder 동일 계약）로\n// 직접 구현한다.\n//\n// 포팅 판단（원본 대비 차이）:\n// - 값 갱신: 드래그 시작 시 measureInWindow로 트랙 위치폭을 캐시하고, onMove의\n//   절대 x로 위치 백분율을 계산한 뒤 min/max/step으로 정규화한다. 제스처 시작\n//   위치에서 가장 가까운 썸이 움직인다（다중 썸 근사） — 인접 썸 경계를 넘지\n//   않도록 클램프한다（썸 교차 없음, radix와 동일 동작）.\n// - 썸 위치범위 채움은 원본의 radix 배치 대신 동적 백분율 left/width\n//   （BridgeStyle） — progress.tsx 관행. 썸 중심 보정（-ml-2/-mt-2 = size-4의\n//   절반 8px）은 표준 계단 토큰으로 표현해 transformcalc를 쓰지 않는다.\n// - orientation（vertical） 미지원 — 원본의 data-［orientation］ 셀렉터 전개는\n//   data 속성이 없는 네이티브에서 재현 불가해 수평 전용으로 단순화했다.\n// - 원본의 ［min,max］ 폴백（radix가 도달시키지 않는 방어 코드） 대신 defaultValue\n//   미지정 시 썸 1개（min）로 시작한다.\n// - 접근성: 루트 Pressable이 role=\"slider\"를 대표해 aria-valuenow/min/max를\n//   부여한다（마지막으로 조작한 썸 기준）. 썸별 포커스썸별 aria는 미지원 —\n//   썸은 장식 View（aria-hidden）이고 조작은 트랙 전체 드래그가 담당한다.\n// - 웹 키보드 조정: 루트 onKeyDown으로 ArrowLeft/Right（±step）Home/End.\n//   포커스 링은 원본의 썸 focus-visible 대신 드래그 중 활성 썸 강조로 근사.\n// - transition-［color,box-shadow］（임의값）hover/focus-visible（웹 변형）은\n//   web: 접두의 표준 토큰으로 근사했다.\nimport {\n  forwardRef,\n  useEffect,\n  useRef,\n  useState,\n  type ReactNode,\n} from \"react\";\nimport {\n  Pressable,\n  View,\n  measureInWindow,\n  useDrag,\n  type BridgeDragEvent,\n  type BridgeKeyEvent,\n  type BridgePressableRef,\n  type PressableProps,\n} from \"@unikit-bridge/ui/primitives\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport type SliderProps = Omit<\n  PressableProps,\n  \"onPress\" | \"children\" | \"onKeyDown\"\n> & {\n  /** 제어형 값（배열 — 다중 썸）. 지정 시 내부 state를 대체한다 */\n  value?: number[];\n  /** 비제어형 초기값 */\n  defaultValue?: number[];\n  /** 값 확정 콜백（값이 변한 이동에서만 발화） */\n  onValueChange?: (value: number[]) => void;\n  /** 최솟값（기본 0） */\n  min?: number;\n  /** 최댓값（기본 100） */\n  max?: number;\n  /** 조정 단위（기본 1） */\n  step?: number;\n  /** 슬라이더 비활성화 */\n  disabled?: boolean;\n  /** 내부 전용 — 루트 children은 트랙/썸로 고정 */\n  children?: ReactNode;\n  /** 웹 키보드 조정（ArrowLeft/RightHome/End） */\n  onKeyDown?: (event: BridgeKeyEvent) => void;\n};\n\nconst Slider = forwardRef<BridgePressableRef, SliderProps>(\n  (\n    {\n      className,\n      value,\n      defaultValue,\n      onValueChange,\n      min = 0,\n      max = 100,\n      step = 1,\n      disabled,\n      ...props\n    },\n    ref,\n  ) => {\n    // 제어/비제어 통합 — 썸 개수는 원본처럼 값 배열 길이로 정한다.\n    const [internal, setInternal] = useState<number[]>(defaultValue ?? [min]);\n    const isControlled = value !== undefined;\n    const values = isControlled ? value : internal;\n\n    const safeStep = step > 0 ? step : 1;\n    const range = max > min ? max - min : 1;\n    // step 소수 정밀도（0.1 계열에서 0.300004가 나오지 않게）\n    const stepPrecision = (String(safeStep).split(\".\")[1] ?? \"\").length;\n    const pctOf = (v: number) => ((v - min) / range) * 100;\n    const snap = (raw: number) => {\n      const clamped = Math.min(max, Math.max(min, raw));\n      const stepped =\n        Math.round((clamped - min) / safeStep) * safeStep + min;\n      return Number((Math.min(max, Math.max(min, stepped))).toFixed(stepPrecision));\n    };\n\n    // 드래그 중 활성 썸 — ref가 진실원（첫 onMove에서 결정）, state는 강조 렌더용.\n    const activeRef = useRef<number>(0);\n    const [activeIndex, setActiveIndex] = useState<number | null>(null);\n    // 트랙 캐시 — 드래그마다 재측정（레이아웃 변화 대응）. 마운트 시에도 1회.\n    const trackRef = useRef<BridgePressableRef>(null);\n    const rectRef = useRef<{ x: number; width: number } | null>(null);\n    const measureTrack = () => {\n      measureInWindow(trackRef.current, (x, _y, width) => {\n        rectRef.current = { x, width };\n      });\n    };\n    useEffect(measureTrack, []);\n\n    const commit = (index: number, next: number) => {\n      const arr = values.slice();\n      arr[index] = next;\n      if (!isControlled) setInternal(arr);\n      onValueChange?.(arr);\n    };\n\n    // 인접 썸 경계 안에서 값 확정（썸 교차 방지）.\n    const updateValue = (index: number, raw: number) => {\n      const snapped = snap(raw);\n      const lower = index > 0 ? values[index - 1]! : min;\n      const upper = index < values.length - 1 ? values[index + 1]! : max;\n      const next = Math.min(Math.max(snapped, lower), upper);\n      activeRef.current = index;\n      setActiveIndex(index);\n      if (next !== values[index]) commit(index, next);\n    };\n\n    const handleMove = (event: BridgeDragEvent) => {\n      const rect = rectRef.current;\n      if (!rect || rect.width <= 0) return;\n      const pct = ((event.x - rect.x) / rect.width) * 100;\n      const raw = min + (Math.min(100, Math.max(0, pct)) / 100) * range;\n      if (activeIndex === null) {\n        // 첫 이동 — 절대 위치에서 가장 가까운 썸을 고정한다.\n        let nearest = 0;\n        let best = Infinity;\n        values.forEach((v, i) => {\n          const d = Math.abs(v - raw);\n          if (d < best) {\n            best = d;\n            nearest = i;\n          }\n        });\n        activeRef.current = nearest;\n        setActiveIndex(nearest);\n      }\n      updateValue(activeRef.current, raw);\n    };\n\n    const { dragHandlers, dragging } = useDrag({\n      onBegin: () => {\n        setActiveIndex(null);\n        measureTrack();\n      },\n      onMove: handleMove,\n      onEnd: () => setActiveIndex(null),\n    });\n\n    // 웹 키보드 조정 — 마지막으로 조작한 썸 기준（기본 첫 썸）.\n    const handleKeyDown = (event: BridgeKeyEvent) => {\n      if (disabled) return;\n      // value 배열 교체로 마지막 활성 썸이 사라진 경우 방어\n      const index = Math.min(activeRef.current, values.length - 1);\n      const current = values[index] ?? values[0] ?? min;\n      let next: number;\n      switch (event.key) {\n        case \"ArrowLeft\":\n          next = current - step;\n          break;\n        case \"ArrowRight\":\n          next = current + step;\n          break;\n        case \"Home\":\n          next = min;\n          break;\n        case \"End\":\n          next = max;\n          break;\n        default:\n          return;\n      }\n      event.preventDefault();\n      updateValue(index, next);\n    };\n\n    const first = values[0] ?? min;\n    const last = values[values.length - 1] ?? min;\n\n    return (\n      // 트랙 전체가 드래그키보드 조작면 — Pressable（button）이라 웹 포커스가 된다.\n      <Pressable\n        ref={ref}\n        accessibilityRole=\"slider\"\n        aria-valuenow={values[activeRef.current] ?? first}\n        aria-valuemin={min}\n        aria-valuemax={max}\n        disabled={disabled}\n        onKeyDown={handleKeyDown}\n        className={cn(\n          \"relative flex w-full touch-none flex-row items-center select-none\",\n          disabled && \"web:pointer-events-none opacity-50\",\n          className,\n        )}\n        {...dragHandlers}\n        {...props}\n      >\n        {/* 트랙 — 원본 h-1.5grow 토큰 그대로 */}\n        <View\n          aria-hidden\n          className=\"relative h-1.5 w-full grow overflow-hidden rounded-full bg-muted\"\n        >\n          {/* 범위 채움 — 동적 백분율 left/width는 BridgeStyle 전용 영역 */}\n          <View\n            className=\"absolute h-full bg-primary\"\n            style={{\n              left: `${pctOf(first)}%`,\n              width: `${Math.max(0, pctOf(last) - pctOf(first))}%`,\n            }}\n          />\n        </View>\n        {/* 썸 — 장식（aria-hidden）. 위치는 동적 백분율 left */}\n        {values.map((v, index) => (\n          <View\n            aria-hidden\n            key={index}\n            className={cn(\n              // 원본 썸: shadow-sm + ring-ring/50 상시·hover/focus-visible ring-4.\n              // 장식 썸은 포커스 불가라 hover로 근사하고, 드래그 링은 기존 어포던스 유지.\n              \"absolute top-1/2 -mt-2 -ml-2 size-4 rounded-full border border-primary bg-white web:ring-ring/50 web:hover:ring-4\",\n              dragging &&\n                index === (activeIndex ?? activeRef.current) &&\n                \"web:ring-4\",\n            )}\n            style={{ left: `${pctOf(v)}%` }}\n          />\n        ))}\n      </Pressable>\n    );\n  },\n);\nSlider.displayName = \"Slider\";\n\nexport { Slider };\n",
      "type": "registry:ui",
      "target": "components/ui/slider.tsx"
    }
  ],
  "type": "registry:ui"
}
