{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "add-task-sheet", "type": "registry:ui", "title": "Add Task Sheet", "description": "Quick-add task dialog — title input, priority pills, Enter to submit. Three zones, two hairlines.", "dependencies": [ "motion" ], "files": [ { "path": "registry/ruixenui/add-task-sheet.tsx", "content": "\"use client\";\n\nimport { useState, useRef, useCallback, useEffect } from \"react\";\nimport { AnimatePresence, motion } from \"motion/react\";\n\n/**\n * Add Task — Rauno Freiberg craft.\n *\n * A quick-add task dialog. Title input, priority pills, submit.\n * Three zones separated by hairlines. Enter to submit.\n * The text field IS the form.\n *\n * Uses absolute positioning — fills its nearest positioned parent.\n * Wrap in a fixed inset-0 container for full-viewport overlay.\n *\n * Auto-focus on open. Escape to close.\n * Submit wakes up when title is non-empty.\n * Soft tick on keystrokes and interactions.\n */\n\n/* ── Types ── */\n\nexport type Priority = \"urgent\" | \"high\" | \"medium\" | \"low\";\n\nexport interface TaskData {\n title: string;\n priority: Priority | null;\n}\n\nexport interface AddTaskSheetProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n onSubmit?: (task: TaskData) => void;\n sound?: boolean;\n}\n\n/* ── Constants ── */\n\nconst W = 400;\nconst PRIORITIES: Priority[] = [\"urgent\", \"high\", \"medium\", \"low\"];\n\n/* ── Audio ── */\n\nlet _ctx: AudioContext | null = null;\nlet _buf: AudioBuffer | null = null;\n\nfunction audioCtx() {\n if (!_ctx) {\n _ctx = new (window.AudioContext ||\n (window as unknown as { webkitAudioContext: typeof AudioContext })\n .webkitAudioContext)();\n }\n if (_ctx.state === \"suspended\") _ctx.resume();\n return _ctx;\n}\n\nfunction ensureBuf(ac: AudioContext): AudioBuffer {\n if (_buf && _buf.sampleRate === ac.sampleRate) return _buf;\n const rate = ac.sampleRate;\n const len = Math.floor(rate * 0.003);\n const buf = ac.createBuffer(1, len, rate);\n const ch = buf.getChannelData(0);\n for (let i = 0; i < len; i++) {\n const t = i / len;\n ch[i] = (Math.random() * 2 - 1) * (1 - t) ** 4;\n }\n _buf = buf;\n return buf;\n}\n\nfunction playTick(last: React.MutableRefObject) {\n const now = performance.now();\n if (now - last.current < 20) return;\n last.current = now;\n try {\n const ac = audioCtx();\n const buf = ensureBuf(ac);\n const src = ac.createBufferSource();\n const gain = ac.createGain();\n src.buffer = buf;\n src.playbackRate.value = 1.0;\n gain.gain.value = 0.04;\n src.connect(gain);\n gain.connect(ac.destination);\n src.start();\n } catch {\n /* silent */\n }\n}\n\n/* ── Component ── */\n\nexport function AddTaskSheet({\n open,\n onOpenChange,\n onSubmit,\n sound = true,\n}: AddTaskSheetProps) {\n const [title, setTitle] = useState(\"\");\n const [priority, setPriority] = useState(null);\n const [focused, setFocused] = useState(false);\n\n const lastSound = useRef(0);\n const inputRef = useRef(null);\n\n const ready = title.trim() !== \"\";\n\n /* Open → focus, close → reset */\n useEffect(() => {\n if (open) {\n const t = setTimeout(() => inputRef.current?.focus(), 120);\n return () => clearTimeout(t);\n }\n setTitle(\"\");\n setPriority(null);\n setFocused(false);\n }, [open]);\n\n /* Escape */\n useEffect(() => {\n if (!open) return;\n const h = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") onOpenChange(false);\n };\n window.addEventListener(\"keydown\", h);\n return () => window.removeEventListener(\"keydown\", h);\n }, [open, onOpenChange]);\n\n /* Submit */\n const submit = useCallback(() => {\n if (!title.trim()) {\n inputRef.current?.focus();\n return;\n }\n if (sound) playTick(lastSound);\n onSubmit?.({ title: title.trim(), priority });\n }, [title, priority, sound, onSubmit]);\n\n /* Enter to submit */\n const onKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (e.key === \"Enter\") {\n e.preventDefault();\n submit();\n }\n },\n [submit],\n );\n\n return (\n <>\n \n \n {open && (\n \n {/* Backdrop */}\n onOpenChange(false)}\n />\n\n {/* Panel */}\n \n {/* ── Title ── */}\n
\n {\n if (sound) playTick(lastSound);\n setTitle(e.target.value);\n }}\n onKeyDown={onKeyDown}\n onFocus={() => setFocused(true)}\n onBlur={() => setFocused(false)}\n style={{\n width: \"100%\",\n background: \"transparent\",\n border: \"none\",\n outline: \"none\",\n fontSize: 15,\n fontWeight: 450,\n letterSpacing: \"-0.01em\",\n color: `rgba(var(--d-ink),${focused ? 0.85 : title ? 0.6 : 0.5})`,\n transition: \"color 0.15s\",\n }}\n />\n
\n\n
\n\n {/* ── Priority ── */}\n \n {PRIORITIES.map((p) => {\n const active = priority === p;\n return (\n {\n if (sound) playTick(lastSound);\n setPriority(active ? null : p);\n }}\n style={{\n fontSize: 11,\n fontWeight: 500,\n letterSpacing: \"0.04em\",\n textTransform: \"uppercase\",\n color: `rgba(var(--d-ink),${active ? 0.7 : 0.2})`,\n background: active\n ? \"rgba(var(--d-ink),0.06)\"\n : \"transparent\",\n border: \"none\",\n borderRadius: 999,\n padding: \"5px 10px\",\n cursor: \"pointer\",\n transition: \"color 0.15s, background 0.15s\",\n }}\n onMouseEnter={(e) => {\n if (!active)\n e.currentTarget.style.color =\n \"rgba(var(--d-ink),0.4)\";\n }}\n onMouseLeave={(e) => {\n if (!active)\n e.currentTarget.style.color =\n \"rgba(var(--d-ink),0.2)\";\n }}\n >\n {p}\n \n );\n })}\n
\n\n
\n\n {/* ── Submit ── */}\n {\n if (ready) {\n e.currentTarget.style.color = \"rgba(var(--d-ink),0.9)\";\n e.currentTarget.style.background =\n \"rgba(var(--d-ink),0.06)\";\n }\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.color = `rgba(var(--d-ink),${ready ? 0.65 : 0.2})`;\n e.currentTarget.style.background = ready\n ? \"rgba(var(--d-ink),0.04)\"\n : \"transparent\";\n }}\n >\n Add task\n \n \n \n )}\n \n \n );\n}\n\nexport default AddTaskSheet;\n", "type": "registry:ui", "target": "components/ruixen/add-task-sheet.tsx" } ] }