{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "color-picker",
  "type": "registry:ui",
  "title": "ColorPicker",
  "description": "انتخاب رنگ برند یا برچسب: نمونه‌رنگ‌ها، پنل انتخاب دلخواه و کد hex، با کنتراست متن روی آن",
  "dependencies": [
    "@kashi-ui/persian@^1.8.0"
  ],
  "registryDependencies": [
    "https://kashi.sepantapouya.com/r/cx.json",
    "https://kashi.sepantapouya.com/r/field.json",
    "https://kashi.sepantapouya.com/r/kashi-provider.json"
  ],
  "files": [
    {
      "path": "registry/kashi/ColorPicker.tsx",
      "type": "registry:ui",
      "target": "components/kashi/ColorPicker.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useId, useRef, useState, type CSSProperties, type KeyboardEvent, type PointerEvent, type ReactNode } from \"react\";\nimport { toFaDigits } from \"@kashi-ui/persian\";\nimport { Field } from \"./Field\";\nimport { cx } from \"./cx\";\nimport { useKashi } from \"./KashiProvider\";\n\nexport interface ColorPickerProps {\n  label?: ReactNode;\n  hint?: ReactNode;\n  /** رنگ hex، مثل \"#0fa89b\" */\n  value?: string;\n  defaultValue?: string;\n  onChange?: (hex: string) => void;\n  /** رنگ‌های پیشنهادی؛ [] برای حذف */\n  swatches?: { name: string; value: string }[];\n  /** نمایش کنتراست با متن سفید و مشکی @default true */\n  showContrast?: boolean;\n  name?: string;\n  id?: string;\n  className?: string;\n}\n\n// نام رنگ‌ها از فرهنگ متن‌ها می‌آید تا با زبان KashiProvider عوض شود\nconst DEFAULT_SWATCHES = [\n  { key: \"firoozeh\", value: \"#0b8077\" },\n  { key: \"lajvard\", value: \"#2740ed\" },\n  { key: \"violet\", value: \"#7c3aed\" },\n  { key: \"anar\", value: \"#e51d2a\" },\n  { key: \"zafaran\", value: \"#dd5e02\" },\n  { key: \"peste\", value: \"#518b25\" },\n  { key: \"shab\", value: \"#2a2f3d\" },\n] as const;\n\nconst HEX = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i;\nconst norm = (v: string) => {\n  const m = v.trim().match(HEX);\n  if (!m) return null;\n  const h = m[1].length === 3 ? [...m[1]].map((c) => c + c).join(\"\") : m[1];\n  return `#${h.toLowerCase()}`;\n};\nconst lum = (hex: string) => {\n  const [r, g, b] = [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255).map((c) => (c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4));\n  return 0.2126 * r + 0.7152 * g + 0.0722 * b;\n};\n/** نسبت کنتراست WCAG */\nexport const contrastRatio = (a: string, b: string) => {\n  a = norm(a) ?? a;\n  b = norm(b) ?? b;\n  const [x, y] = [lum(a), lum(b)].sort((m, n) => n - m);\n  return (x + 0.05) / (y + 0.05);\n};\n\n// تبدیل‌های رنگ برای پنل انتخاب\ntype HSV = { h: number; s: number; v: number };\nconst hexToRgb = (hex: string) => [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16)) as [number, number, number];\nconst rgbToHex = (r: number, g: number, b: number) => `#${[r, g, b].map((n) => Math.round(Math.min(255, Math.max(0, n))).toString(16).padStart(2, \"0\")).join(\"\")}`;\nfunction hexToHsv(hex: string): HSV {\n  const [r, g, b] = hexToRgb(hex).map((n) => n / 255);\n  const max = Math.max(r, g, b), d = max - Math.min(r, g, b);\n  let h = 0;\n  if (d) h = max === r ? ((g - b) / d) % 6 : max === g ? (b - r) / d + 2 : (r - g) / d + 4;\n  return { h: (h * 60 + 360) % 360, s: max ? d / max : 0, v: max };\n}\nfunction hsvToHex({ h, s, v }: HSV) {\n  const f = (n: number) => {\n    const k = (n + h / 60) % 6;\n    return (v - v * s * Math.max(0, Math.min(k, 4 - k, 1))) * 255;\n  };\n  return rgbToHex(f(5), f(3), f(1));\n}\n\nconst Dropper = () => <svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" strokeWidth={1.7} strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\"><path d=\"m11 5 4 4M13.5 2.5a2.1 2.1 0 0 1 3 3L15 7 13 5ZM12 6l-7.5 7.5L3 17l3.5-1.5L14 8\" /></svg>;\n\n/** پنل انتخاب رنگ: سطح اشباع و روشنایی، نوار رنگ‌مایه، قطره‌چکان و کدها */\nfunction ColorPanel({ hex, onChange, onClose }: { hex: string; onChange: (hex: string) => void; onClose: () => void }) {\n  const { messages: M, n: N } = useKashi();\n  const C = M.colorPicker;\n  const [hsv, setHsv] = useState(() => hexToHsv(hex));\n  const area = useRef<HTMLDivElement>(null);\n  const panel = useRef<HTMLDivElement>(null);\n  // تغییر از بیرون (نمونه‌رنگ یا فیلد hex) رنگ‌مایه را حفظ می‌کند وقتی رنگ خاکستری است\n  useEffect(() => {\n    if (hsvToHex(hsv) !== hex) setHsv((p) => { const n = hexToHsv(hex); return n.s === 0 ? { ...n, h: p.h } : n; });\n  }, [hex]);\n  useEffect(() => {\n    area.current?.focus({ preventScroll: true });\n    const onDown = (e: globalThis.PointerEvent) => !panel.current?.parentElement?.contains(e.target as Node) && onClose();\n    document.addEventListener(\"pointerdown\", onDown);\n    return () => document.removeEventListener(\"pointerdown\", onDown);\n  }, []);\n  const apply = (n: HSV) => {\n    setHsv(n);\n    onChange(hsvToHex(n));\n  };\n  const fromPointer = (e: PointerEvent) => {\n    const r = area.current!.getBoundingClientRect();\n    const x = Math.min(1, Math.max(0, (e.clientX - r.left) / r.width));\n    const y = Math.min(1, Math.max(0, (e.clientY - r.top) / r.height));\n    apply({ ...hsv, s: x, v: 1 - y });\n  };\n  const onAreaKey = (e: KeyboardEvent) => {\n    const step = e.shiftKey ? 0.1 : 0.01;\n    const k: Record<string, Partial<HSV>> = {\n      ArrowLeft: { s: Math.max(0, hsv.s - step) }, ArrowRight: { s: Math.min(1, hsv.s + step) },\n      ArrowUp: { v: Math.min(1, hsv.v + step) }, ArrowDown: { v: Math.max(0, hsv.v - step) },\n    };\n    if (k[e.key]) {\n      e.preventDefault();\n      apply({ ...hsv, ...k[e.key] });\n    }\n  };\n  const [r, g, b] = hexToRgb(hex);\n  const pct = (x: number) => M.common.percent(N(Math.round(x * 100)));\n  const eye = typeof window !== \"undefined\" && \"EyeDropper\" in window;\n  return (\n    <div\n      ref={panel}\n      className=\"k-color__panel\"\n      role=\"dialog\"\n      aria-label={M.colorPicker.custom}\n      onKeyDown={(e) => {\n        if (e.key === \"Escape\") {\n          e.stopPropagation();\n          onClose();\n        }\n      }}\n    >\n      <div\n        ref={area}\n        className=\"k-color__area\"\n        style={{ \"--_hue\": `hsl(${hsv.h} 100% 50%)` } as CSSProperties}\n        role=\"slider\"\n        tabIndex={0}\n        aria-label={C.area}\n        aria-valuetext={C.areaValue(pct(hsv.s), pct(hsv.v))}\n        aria-valuenow={Math.round(hsv.s * 100)}\n        onPointerDown={(e) => {\n          e.currentTarget.setPointerCapture(e.pointerId);\n          fromPointer(e);\n        }}\n        onPointerMove={(e) => e.buttons === 1 && fromPointer(e)}\n        onKeyDown={onAreaKey}\n      >\n        <span className=\"k-color__thumb\" style={{ left: `${hsv.s * 100}%`, top: `${(1 - hsv.v) * 100}%` }} />\n      </div>\n      <div className=\"k-color__tools\">\n        {eye && (\n          <button\n            type=\"button\"\n            className=\"k-btn k-btn--tertiary k-btn--icon k-btn--sm\"\n            aria-label={C.eyedropper}\n            onClick={async () => {\n              try {\n                const res = await new (window as unknown as { EyeDropper: new () => { open: () => Promise<{ sRGBHex: string }> } }).EyeDropper().open();\n                const h = norm(res.sRGBHex);\n                if (h) onChange(h);\n              } catch { /* کاربر انصراف داد */ }\n            }}\n          >\n            <Dropper />\n          </button>\n        )}\n        <span className=\"k-color__current\" aria-hidden=\"true\" />\n        <input\n          type=\"range\"\n          className=\"k-color__hue\"\n          min={0}\n          max={359}\n          value={Math.round(hsv.h)}\n          aria-label={C.hue}\n          aria-valuetext={C.degrees(N(Math.round(hsv.h)))}\n          onChange={(e) => apply({ ...hsv, h: Number(e.target.value) })}\n        />\n      </div>\n      <div className=\"k-color__codes\" dir=\"ltr\">\n        {([\"R\", \"G\", \"B\"] as const).map((ch, i) => (\n          <label key={ch} className=\"k-color__code\">\n            <input\n              className=\"k-color__num\"\n              inputMode=\"numeric\"\n              value={[r, g, b][i]}\n              aria-label={{ R: C.red, G: C.green, B: C.blue }[ch]}\n              onChange={(e) => {\n                const n = Math.min(255, Math.max(0, Number(e.target.value.replace(/\\D/g, \"\")) || 0));\n                const rgb = [r, g, b];\n                rgb[i] = n;\n                onChange(rgbToHex(rgb[0], rgb[1], rgb[2]));\n              }}\n            />\n            <span aria-hidden=\"true\">{ch}</span>\n          </label>\n        ))}\n      </div>\n    </div>\n  );\n}\n\nconst Check = () => <svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" strokeWidth={2.2} strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\"><path d=\"m5 10.5 3.2 3L15 7\" /></svg>;\n\n/** انتخاب رنگ برند یا برچسب: نمونه‌رنگ‌ها، پنل انتخاب دلخواه و کد hex، با کنتراست متن روی آن */\nexport function ColorPicker({ label, hint, value, defaultValue = \"#0b8077\", onChange, swatches: swatchesProp, showContrast = true, name, id, className }: ColorPickerProps) {\n  const { messages: M, n: N } = useKashi();\n  const swatches = swatchesProp ?? DEFAULT_SWATCHES.map((s) => ({ name: M.colorPicker.names[s.key], value: s.value }));\n  const autoId = useId();\n  const fieldId = id ?? autoId;\n  const [inner, setInner] = useState(norm(defaultValue) ?? \"#000000\");\n  const current = norm(value ?? \"\") ?? inner;\n  const [text, setText] = useState<string | null>(null);\n  const [open, setOpen] = useState(false);\n  const trigger = useRef<HTMLButtonElement>(null);\n  const set = (hex: string) => {\n    if (value === undefined) setInner(hex);\n    setText(null);\n    onChange?.(hex);\n  };\n  const white = contrastRatio(current, \"#ffffff\"), black = contrastRatio(current, \"#000000\");\n  const ink = white >= black ? \"#ffffff\" : \"#000000\";\n  const fmt = (r: number) => N((Math.round(r * 10) / 10).toFixed(1)).replace(\".\", M.common.decimal);\n  const invalid = text !== null && !norm(text);\n  return (\n    <Field id={`${fieldId}-hex`} label={label} hint={hint} error={invalid ? M.colorPicker.invalid : undefined} className={className}>\n      <div className=\"k-color\" style={{ \"--_sw\": current, \"--_ink\": ink } as CSSProperties}>\n        {swatches.length > 0 && (\n          <div className=\"k-color__swatches\" role=\"radiogroup\" aria-label={M.colorPicker.swatches}>\n            {swatches.map((s) => (\n              <label key={s.value} className=\"k-color__swatch\" style={{ \"--_sw\": s.value, \"--_ink\": contrastRatio(s.value, \"#ffffff\") >= 3 ? \"#fff\" : \"#000\" } as CSSProperties} title={s.name}>\n                <input type=\"radio\" className=\"k-sr-only\" name={`${fieldId}-sw`} checked={norm(s.value) === current} aria-label={s.name} onChange={() => set(norm(s.value)!)} />\n                <Check />\n              </label>\n            ))}\n          </div>\n        )}\n        <div className=\"k-color__row\">\n          <span className=\"k-color__anchor\">\n            <button\n              ref={trigger}\n              type=\"button\"\n              className=\"k-color__preview\"\n              aria-label={M.colorPicker.custom}\n              aria-haspopup=\"dialog\"\n              aria-expanded={open}\n              onClick={() => setOpen((o) => !o)}\n            />\n            {open && <ColorPanel hex={current} onChange={set} onClose={() => { setOpen(false); trigger.current?.focus(); }} />}\n          </span>\n          <div className=\"k-input\" data-state={invalid ? \"invalid\" : undefined}>\n            <input\n              id={`${fieldId}-hex`}\n              className=\"k-input__control\"\n              dir=\"ltr\"\n              spellCheck={false}\n              autoComplete=\"off\"\n              value={text ?? current}\n              aria-invalid={invalid || undefined}\n              onChange={(e) => {\n                setText(e.target.value);\n                const h = norm(e.target.value);\n                if (h) {\n                  if (value === undefined) setInner(h);\n                  onChange?.(h);\n                }\n              }}\n              onBlur={() => !invalid && setText(null)}\n            />\n          </div>\n        </div>\n        {showContrast && (\n          <p className=\"k-color__contrast\" aria-live=\"polite\">\n            <span className=\"k-color__sample\">{M.colorPicker.sample}</span>\n            <span>{M.colorPicker.contrast(ink === \"#ffffff\" ? \"white\" : \"black\", fmt(Math.max(white, black)), Math.max(white, black) >= 4.5)}</span>\n          </p>\n        )}\n        {name && <input type=\"hidden\" name={name} value={current} />}\n      </div>\n    </Field>\n  );\n}\n"
    }
  ]
}