{ "$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