{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "better-time-picker",
  "type": "registry:ui",
  "title": "Better Time Picker",
  "description": "iOS 26 liquid-glass time picker with column steppers, spring-animated values, and segmented AM/PM toggle.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/ruixenui/better-time-picker.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { motion } from \"motion/react\";\n\n/* ── sound ── */\nlet _a: AudioContext, _b: AudioBuffer;\nconst tick = () => {\n  if (typeof window === \"undefined\") return;\n  if (!_a) {\n    _a = new AudioContext();\n    _b = _a.createBuffer(1, (_a.sampleRate * 0.003) | 0, _a.sampleRate);\n    const d = _b.getChannelData(0);\n    for (let i = 0; i < d.length; i++)\n      d[i] = (Math.random() * 2 - 1) * (1 - i / d.length) ** 4;\n  }\n  const s = _a.createBufferSource();\n  s.buffer = _b;\n  const g = _a.createGain();\n  g.gain.value = 0.08;\n  s.connect(g).connect(_a.destination);\n  s.start();\n};\n\n/* ── theme ── */\nconst CSS = `\n.tp{\n  --tp-glass:linear-gradient(180deg,rgba(255,255,255,0.78),rgba(255,255,255,0.62));\n  --tp-border:rgba(0,0,0,0.06);\n  --tp-shadow:0 0 1px rgba(0,0,0,0.04),0 2px 8px rgba(0,0,0,0.04),inset 0 1px 0 rgba(255,255,255,0.8);\n  --tp-dim:rgba(0,0,0,0.42);\n  --tp-mid:rgba(0,0,0,0.55);\n  --tp-hi:rgba(0,0,0,0.88);\n  --tp-sep:rgba(0,0,0,0.06);\n  --tp-btn:rgba(0,0,0,0.03);\n  --tp-btn-h:rgba(0,0,0,0.07);\n  --tp-seg-bg:rgba(0,0,0,0.04);\n  --tp-seg-active:rgba(0,0,0,0.07);\n  --tp-knob:#fff;\n  --tp-knob-border:rgba(0,0,0,0.08);\n  --tp-knob-shadow:0 1px 3px rgba(0,0,0,0.12)\n}\n.dark .tp,[data-theme=\"dark\"] .tp{\n  --tp-glass:linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.02));\n  --tp-border:rgba(255,255,255,0.07);\n  --tp-shadow:0 1px 3px rgba(0,0,0,0.08),inset 0 1px 0 rgba(255,255,255,0.04);\n  --tp-dim:rgba(255,255,255,0.28);\n  --tp-mid:rgba(255,255,255,0.5);\n  --tp-hi:rgba(255,255,255,0.88);\n  --tp-sep:rgba(255,255,255,0.06);\n  --tp-btn:rgba(255,255,255,0.03);\n  --tp-btn-h:rgba(255,255,255,0.07);\n  --tp-seg-bg:rgba(255,255,255,0.04);\n  --tp-seg-active:rgba(255,255,255,0.09);\n  --tp-knob:#fff;\n  --tp-knob-border:rgba(255,255,255,0.06);\n  --tp-knob-shadow:0 1px 3px rgba(0,0,0,0.15)\n}`;\n\nconst MONO =\n  \"ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, monospace\";\n\n/* ── types ── */\ninterface BetterTimePickerProps {\n  defaultHour?: number;\n  defaultMinute?: number;\n  use24?: boolean;\n  minuteStep?: number;\n  onChange?: (hour: number, minute: number) => void;\n  sound?: boolean;\n  style?: React.CSSProperties;\n}\n\n/* ── component ── */\nexport default function BetterTimePicker({\n  defaultHour = 12,\n  defaultMinute = 0,\n  use24 = false,\n  minuteStep = 5,\n  onChange,\n  sound = true,\n  style,\n}: BetterTimePickerProps) {\n  const [hour, setHour] = React.useState(defaultHour);\n  const [minute, setMinute] = React.useState(defaultMinute);\n  const [period, setPeriod] = React.useState<\"AM\" | \"PM\">(\n    defaultHour >= 12 ? \"PM\" : \"AM\",\n  );\n\n  const displayHour = use24\n    ? hour\n    : hour === 0\n      ? 12\n      : hour > 12\n        ? hour - 12\n        : hour;\n\n  const stepHour = (dir: 1 | -1) => {\n    const maxH = use24 ? 23 : 23;\n    const next = (hour + dir + 24) % 24;\n    setHour(next);\n    if (!use24) setPeriod(next >= 12 ? \"PM\" : \"AM\");\n    if (sound) tick();\n    onChange?.(next, minute);\n  };\n\n  const stepMinute = (dir: 1 | -1) => {\n    const next = (minute + dir * minuteStep + 60) % 60;\n    setMinute(next);\n    if (sound) tick();\n    onChange?.(hour, next);\n  };\n\n  const togglePeriod = () => {\n    const newPeriod = period === \"AM\" ? \"PM\" : \"AM\";\n    setPeriod(newPeriod);\n    const newHour = newPeriod === \"PM\" ? (hour % 12) + 12 : hour % 12;\n    setHour(newHour);\n    if (sound) tick();\n    onChange?.(newHour, minute);\n  };\n\n  return (\n    <>\n      <style dangerouslySetInnerHTML={{ __html: CSS }} />\n      <motion.div\n        className=\"tp\"\n        initial={{ opacity: 0, y: 8 }}\n        animate={{ opacity: 1, y: 0 }}\n        transition={{ duration: 0.35, ease: [0.25, 0.1, 0.25, 1] }}\n        style={{\n          display: \"inline-flex\",\n          flexDirection: \"column\",\n          gap: 0,\n          borderRadius: 14,\n          background: \"var(--tp-glass)\",\n          border: \"1px solid var(--tp-border)\",\n          boxShadow: \"var(--tp-shadow)\",\n          backdropFilter: \"blur(24px)\",\n          WebkitBackdropFilter: \"blur(24px)\",\n          overflow: \"hidden\",\n          ...style,\n        }}\n      >\n        {/* header */}\n        <div style={{ padding: \"12px 16px\" }}>\n          <div\n            style={{\n              display: \"flex\",\n              alignItems: \"center\",\n              gap: 8,\n            }}\n          >\n            <svg\n              width={14}\n              height={14}\n              viewBox=\"0 0 16 16\"\n              fill=\"none\"\n              stroke=\"var(--tp-dim)\"\n              strokeWidth={1.5}\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n            >\n              <circle cx=\"8\" cy=\"8\" r=\"6.5\" />\n              <path d=\"M8 4.5V8l2.5 1.5\" />\n            </svg>\n            <span\n              style={{ fontSize: 13, fontWeight: 500, color: \"var(--tp-hi)\" }}\n            >\n              Time\n            </span>\n          </div>\n        </div>\n\n        <div style={{ height: 1, background: \"var(--tp-sep)\" }} />\n\n        {/* picker columns */}\n        <div\n          style={{\n            display: \"flex\",\n            alignItems: \"center\",\n            justifyContent: \"center\",\n            padding: \"16px 20px\",\n            gap: 4,\n          }}\n        >\n          {/* hour column */}\n          <TimeColumn\n            value={String(displayHour).padStart(2, \"0\")}\n            onUp={() => stepHour(1)}\n            onDown={() => stepHour(-1)}\n          />\n\n          {/* colon */}\n          <span\n            style={{\n              fontSize: 24,\n              fontWeight: 500,\n              fontFamily: MONO,\n              color: \"var(--tp-dim)\",\n              padding: \"0 2px\",\n              userSelect: \"none\",\n            }}\n          >\n            :\n          </span>\n\n          {/* minute column */}\n          <TimeColumn\n            value={String(minute).padStart(2, \"0\")}\n            onUp={() => stepMinute(1)}\n            onDown={() => stepMinute(-1)}\n          />\n\n          {/* AM/PM toggle */}\n          {!use24 && (\n            <div\n              style={{\n                marginLeft: 12,\n                borderRadius: 8,\n                background: \"var(--tp-seg-bg)\",\n                display: \"flex\",\n                flexDirection: \"column\",\n                overflow: \"hidden\",\n                position: \"relative\",\n              }}\n            >\n              {([\"AM\", \"PM\"] as const).map((p) => (\n                <button\n                  key={p}\n                  onClick={period !== p ? togglePeriod : undefined}\n                  style={{\n                    padding: \"6px 10px\",\n                    fontSize: 11,\n                    fontWeight: 600,\n                    border: \"none\",\n                    background: \"transparent\",\n                    color: period === p ? \"var(--tp-hi)\" : \"var(--tp-dim)\",\n                    cursor: period !== p ? \"pointer\" : \"default\",\n                    position: \"relative\",\n                    zIndex: 1,\n                    transition: \"color 0.15s\",\n                  }}\n                >\n                  {p}\n                </button>\n              ))}\n              {/* active indicator */}\n              <motion.div\n                animate={{ y: period === \"AM\" ? 0 : \"100%\" }}\n                transition={{ type: \"spring\", stiffness: 500, damping: 35 }}\n                style={{\n                  position: \"absolute\",\n                  top: 0,\n                  left: 2,\n                  right: 2,\n                  height: \"50%\",\n                  borderRadius: 6,\n                  background: \"var(--tp-knob)\",\n                  border: \"1px solid var(--tp-knob-border)\",\n                  boxShadow: \"var(--tp-knob-shadow)\",\n                }}\n              />\n            </div>\n          )}\n        </div>\n      </motion.div>\n    </>\n  );\n}\n\n/* ── time column with up/down buttons ── */\nfunction TimeColumn({\n  value,\n  onUp,\n  onDown,\n}: {\n  value: string;\n  onUp: () => void;\n  onDown: () => void;\n}) {\n  return (\n    <div\n      style={{\n        display: \"flex\",\n        flexDirection: \"column\",\n        alignItems: \"center\",\n        gap: 4,\n      }}\n    >\n      <motion.button\n        onClick={onUp}\n        whileTap={{ scale: 0.85 }}\n        style={{\n          width: 36,\n          height: 24,\n          borderRadius: 6,\n          border: \"none\",\n          background: \"var(--tp-btn)\",\n          cursor: \"pointer\",\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          color: \"var(--tp-mid)\",\n          transition: \"background 0.15s\",\n        }}\n        onMouseEnter={(e) => {\n          (e.currentTarget as HTMLElement).style.background = \"var(--tp-btn-h)\";\n        }}\n        onMouseLeave={(e) => {\n          (e.currentTarget as HTMLElement).style.background = \"var(--tp-btn)\";\n        }}\n      >\n        <svg\n          width={10}\n          height={10}\n          viewBox=\"0 0 16 16\"\n          fill=\"none\"\n          stroke=\"currentColor\"\n          strokeWidth={2}\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        >\n          <path d=\"M4 10l4-4 4 4\" />\n        </svg>\n      </motion.button>\n\n      <motion.span\n        key={value}\n        initial={{ opacity: 0.5, y: -4 }}\n        animate={{ opacity: 1, y: 0 }}\n        transition={{ type: \"spring\", stiffness: 500, damping: 30 }}\n        style={{\n          fontSize: 28,\n          fontWeight: 600,\n          fontFamily: MONO,\n          fontVariantNumeric: \"tabular-nums\",\n          color: \"var(--tp-hi)\",\n          lineHeight: 1,\n          userSelect: \"none\",\n          minWidth: 44,\n          textAlign: \"center\",\n        }}\n      >\n        {value}\n      </motion.span>\n\n      <motion.button\n        onClick={onDown}\n        whileTap={{ scale: 0.85 }}\n        style={{\n          width: 36,\n          height: 24,\n          borderRadius: 6,\n          border: \"none\",\n          background: \"var(--tp-btn)\",\n          cursor: \"pointer\",\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          color: \"var(--tp-mid)\",\n          transition: \"background 0.15s\",\n        }}\n        onMouseEnter={(e) => {\n          (e.currentTarget as HTMLElement).style.background = \"var(--tp-btn-h)\";\n        }}\n        onMouseLeave={(e) => {\n          (e.currentTarget as HTMLElement).style.background = \"var(--tp-btn)\";\n        }}\n      >\n        <svg\n          width={10}\n          height={10}\n          viewBox=\"0 0 16 16\"\n          fill=\"none\"\n          stroke=\"currentColor\"\n          strokeWidth={2}\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        >\n          <path d=\"M4 6l4 4 4-4\" />\n        </svg>\n      </motion.button>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ruixen/better-time-picker.tsx"
    }
  ]
}