{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "fine-tune-card", "type": "registry:component", "title": "Fine-tune Card", "description": "The agent adjusts design properties in an inspector.", "registryDependencies": [ "https://www.beautifului.dev/r/foundation.json", "https://www.beautifului.dev/r/glide-menu.json" ], "files": [ { "path": "components/primitives/FineTuneCard.tsx", "content": "\"use client\";\n\nimport { useRef, useState } from \"react\";\nimport GlideMenu from \"@/components/primitives/GlideMenu\";\n\n/* ─────────────────────────────────────────────────────────\n * FINE-TUNE CARD — compact interactive inspector.\n * Number fields scrub: hover the label for an ↔ cursor and\n * drag to adjust, use ↑/↓ (⇧ for ×10), or type directly.\n * ───────────────────────────────────────────────────────── */\n\nfunction ScrubField({\n label,\n value,\n onChange,\n min,\n max,\n step = 1,\n suffix = \"\",\n active,\n}: {\n label: string;\n value: number;\n onChange: (v: number) => void;\n min: number;\n max: number;\n step?: number;\n suffix?: string;\n active?: boolean;\n}) {\n const drag = useRef<{ x: number; v: number } | null>(null);\n const clamp = (v: number) => Math.min(max, Math.max(min, Math.round(v)));\n\n return (\n \n {/* scrub handle */}\n {\n (e.target as HTMLElement).setPointerCapture(e.pointerId);\n drag.current = { x: e.clientX, v: value };\n }}\n onPointerMove={(e) => {\n if (!drag.current) return;\n onChange(clamp(drag.current.v + ((e.clientX - drag.current.x) / 2) * step));\n }}\n onPointerUp={() => (drag.current = null)}\n onKeyDown={(e) => {\n const mult = e.shiftKey ? 10 : 1;\n if (e.key === \"ArrowUp\" || e.key === \"ArrowRight\") {\n e.preventDefault();\n onChange(clamp(value + step * mult));\n } else if (e.key === \"ArrowDown\" || e.key === \"ArrowLeft\") {\n e.preventDefault();\n onChange(clamp(value - step * mult));\n }\n }}\n className=\"flex h-full shrink-0 cursor-ew-resize touch-none items-center rounded-[4px]\n px-0.5 text-[12px] text-ink-3 select-none hover:text-ink-2 focus-visible:text-accent-ink\n focus-visible:outline-none\"\n >\n {label}\n \n {\n const n = Number(e.target.value.replace(/[^\\d-]/g, \"\"));\n if (!Number.isNaN(n)) onChange(clamp(n));\n }}\n aria-label={`${label} value`}\n className=\"min-w-0 flex-1 bg-transparent text-[12px] text-ink tabular-nums outline-none\"\n />\n {suffix && {suffix}}\n \n );\n}\n\nconst SEGMENTS = [\"row\", \"col\", \"grid\"] as const;\n\nfunction SegmentIcon({ kind }: { kind: string }) {\n const dot = \"size-1.5 rounded-[2px] border-[1.2px] border-current\";\n if (kind === \"row\")\n return {[0, 1, 2].map((i) => )};\n if (kind === \"col\")\n return {[0, 1].map((i) => )};\n return (\n \n {[0, 1, 2, 3].map((i) => )}\n \n );\n}\n\n/* A single scrub-able number property. `value` is the initial/default value. */\nexport type FineTuneField = {\n key: string;\n label: string;\n value: number;\n min: number;\n max: number;\n step?: number;\n suffix?: string;\n};\n/* Prominent copy strings on the card. */\nexport type FineTuneCardLabels = {\n title: string;\n layout: string;\n type: string;\n placeholder: string;\n adjust: string;\n edited: string;\n};\n/* The editable state emitted by `onChange`. */\nexport type FineTuneState = {\n segment: number;\n values: Record;\n type: string;\n};\n\nconst FIELDS: FineTuneField[] = [\n { key: \"width\", label: \"W\", value: 324, min: 40, max: 999 },\n { key: \"height\", label: \"H\", value: 96, min: 24, max: 999 },\n { key: \"radius\", label: \"Radius\", value: 28, min: 0, max: 64 },\n { key: \"opacity\", label: \"Opacity\", value: 100, min: 0, max: 100, suffix: \"%\" },\n];\n\nconst OPTIONS = [\"Seasonal\", \"Classic\", \"Limited\"];\n\nconst DEFAULT_LABELS: FineTuneCardLabels = {\n title: \"Flavor card\",\n layout: \"Layout\",\n type: \"Type\",\n placeholder: \"Select type\",\n adjust: \"Adjust\",\n edited: \"Edited\",\n};\n\nfunction chunk(items: T[], size: number): T[][] {\n const rows: T[][] = [];\n for (let i = 0; i < items.length; i += size) rows.push(items.slice(i, i + size));\n return rows;\n}\n\nexport type FineTuneCardProps = {\n /** Accepted for gallery/registry parity; not used by this card. */\n variant?: string;\n /** The scrub-able properties shown in the layout grid (rendered in pairs). */\n fields?: FineTuneField[];\n /** Options offered in the Type menu. */\n options?: string[];\n /** Prominent copy strings. */\n labels?: Partial;\n /** Called with the full editable state whenever the user edits it. */\n onChange?: (state: FineTuneState) => void;\n};\n\nexport default function FineTuneCard({\n fields = FIELDS,\n options = OPTIONS,\n labels,\n onChange,\n}: FineTuneCardProps) {\n const text = { ...DEFAULT_LABELS, ...labels };\n const [seg, setSeg] = useState(0);\n const [values, setValues] = useState>(() =>\n Object.fromEntries(fields.map((f) => [f.key, f.value])),\n );\n const [menuOpen, setMenuOpen] = useState(false);\n const [typeValue, setTypeValue] = useState(text.placeholder);\n\n const selectSeg = (i: number) => {\n setSeg(i);\n onChange?.({ segment: i, values, type: typeValue });\n };\n const setValue = (key: string, v: number) => {\n setValues((current) => {\n const next = { ...current, [key]: v };\n onChange?.({ segment: seg, values: next, type: typeValue });\n return next;\n });\n };\n const selectType = (value: string) => {\n setTypeValue(value);\n setMenuOpen(false);\n onChange?.({ segment: seg, values, type: value });\n };\n\n const changed = fields.some((f) => values[f.key] !== f.value);\n const done = seg !== 0 || changed || typeValue !== text.placeholder;\n\n return (\n
\n {/* header */}\n
\n {text.title}\n {done ? (\n \n \n \n \n {text.edited}\n \n ) : (\n \n \n \n \n \n \n \n {text.adjust}\n \n \n )}\n
\n\n {/* layout section */}\n
\n

{text.layout}

\n {/* segmented control: gray track, raised white thumb */}\n
\n \n {SEGMENTS.map((s, i) => (\n selectSeg(i)}\n className={`relative z-10 flex h-6 items-center justify-center transition-colors duration-200\n ${i === seg ? \"text-accent\" : \"text-ink-3\"}`}\n >\n \n \n ))}\n
\n {chunk(fields, 2).map((pair, ri) => (\n
\n {pair.map((f) => (\n setValue(f.key, v)}\n min={f.min}\n max={f.max}\n step={f.step}\n suffix={f.suffix}\n active={values[f.key] !== f.value}\n />\n ))}\n
\n ))}\n
\n\n {/* interaction section */}\n
\n {text.type}\n
\n setMenuOpen((current) => !current)}\n className=\"flex h-6.5 w-full items-center justify-between rounded-chip bg-inset py-1 pr-1 pl-2\n shadow-hairline transition-shadow duration-200 focus-visible:outline-none\"\n style={{ boxShadow: menuOpen ? \"0 0 0 1px var(--accent)\" : undefined }}\n >\n \n {typeValue}\n \n \n \n \n \n\n {menuOpen && (\n \n \n {options.map((item) => (\n selectType(item)}\n className={`relative z-10 flex h-6.5 w-full items-center rounded-[6px] px-2 text-left text-[12.5px] text-ink ${\n item === typeValue ? \"bg-field group-hover/glide-menu:bg-transparent\" : \"\"\n }`}\n >\n {item}\n \n ))}\n \n
\n )}\n
\n
\n \n );\n}\n", "type": "registry:component", "target": "components/primitives/FineTuneCard.tsx" } ] }