{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"fine-tune-card","title":"Fine-tune Card","description":"The agent adjusts design properties in an inspector.","dependencies":[],"registryDependencies":["https://ward.so/r/foundation.json","https://ward.so/r/glide-menu.json"],"files":[{"path":"registry/ward/FineTuneCard.tsx","content":"\"use client\";\n/*\n * MIT License\n *\n * Copyright (c) 2026 Shane Levine\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\nimport \"./styles/foundation.css\";\n\nimport { useRef, useState } from \"react\";\nimport GlideMenu from \"./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    <label\n      className=\"flex h-6.5 min-w-0 items-center gap-1 rounded-chip py-1 pr-1 pl-0.5\n        transition-[background-color,box-shadow] duration-200\"\n      style={{\n        background: active ? \"var(--highlight-tint)\" : \"var(--field)\",\n        boxShadow: active ? \"0 0 0 1px var(--highlight)\" : \"none\",\n      }}\n    >\n      {/* scrub handle */}\n      <span\n        role=\"slider\"\n        aria-label={label}\n        aria-valuenow={value}\n        aria-valuemin={min}\n        aria-valuemax={max}\n        tabIndex={0}\n        onPointerDown={(e) => {\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-highlight-ink\n          focus-visible:outline-none\"\n      >\n        {label}\n      </span>\n      <input\n        inputMode=\"numeric\"\n        value={value}\n        onChange={(e) => {\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 && <span className=\"shrink-0 pr-0.5 text-[11.5px] text-ink-3\">{suffix}</span>}\n    </label>\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 <span className=\"flex gap-0.5\">{[0, 1, 2].map((i) => <span key={i} className={dot} />)}</span>;\n  if (kind === \"col\")\n    return <span className=\"flex flex-col gap-0.5\">{[0, 1].map((i) => <span key={i} className={dot} />)}</span>;\n  return (\n    <span className=\"grid grid-cols-2 gap-0.5\">\n      {[0, 1, 2, 3].map((i) => <span key={i} className={dot} />)}\n    </span>\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<string, number>;\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: \"Project card\",\n  layout: \"Layout\",\n  type: \"Type\",\n  placeholder: \"Select type\",\n  adjust: \"Adjust\",\n  edited: \"Edited\",\n};\n\nfunction chunk<T>(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<FineTuneCardLabels>;\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<Record<string, number>>(() =>\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    <div className=\"relative w-full max-w-60 rounded-card bg-card shadow-raised\">\n      {/* header */}\n      <div className=\"primitive-card-bar flex items-center justify-between border-b border-line\">\n        <span className=\"text-[13px] font-medium text-ink\">{text.title}</span>\n        {done ? (\n          <span\n            className=\"flex items-center gap-1.5 text-[12px] font-medium text-green\"\n            style={{ animation: \"pop-in 250ms cubic-bezier(0.23,1,0.32,1) both\" }}\n          >\n            <svg width=\"10\" height=\"10\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"3\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n              <path d=\"M20 6L9 17l-5-5\" />\n            </svg>\n            {text.edited}\n          </span>\n        ) : (\n          <span className=\"flex items-center gap-1.5\">\n            <span className=\"flex size-4.5 items-center justify-center rounded-[5px] border border-highlight/30 bg-highlight-tint\">\n              <svg width=\"9\" height=\"9\" viewBox=\"0 0 24 24\" fill=\"var(--highlight)\">\n                <path d=\"M12 2l2.4 7.2L22 12l-7.6 2.8L12 22l-2.4-7.2L2 12l7.6-2.8z\" />\n              </svg>\n            </span>\n            <span\n              className=\"bg-clip-text text-[12px] font-medium text-transparent\"\n              style={{\n                backgroundImage:\n                  \"linear-gradient(90deg, var(--highlight) 35%, var(--highlight-ink) 50%, var(--highlight) 65%)\",\n                backgroundSize: \"200% 100%\",\n                animation: \"shimmer-text 1.4s linear infinite\",\n              }}\n            >\n              {text.adjust}\n            </span>\n          </span>\n        )}\n      </div>\n\n      {/* layout section */}\n      <div className=\"primitive-card-pad flex flex-col gap-2 border-b border-line\">\n        <p className=\"text-[12.5px] font-medium text-ink\">{text.layout}</p>\n        {/* segmented control: gray track, raised white thumb */}\n        <div className=\"relative grid grid-cols-3 rounded-control bg-field p-0.5\">\n          <span\n            aria-hidden\n            className=\"absolute inset-y-0.5 rounded-[6px] bg-card shadow-btn transition-transform duration-300\"\n            style={{\n              width: \"calc((100% - 4px) / 3)\",\n              left: 2,\n              transform: `translateX(${seg * 100}%)`,\n              transitionTimingFunction: \"cubic-bezier(0.23, 1, 0.32, 1)\",\n            }}\n          />\n          {SEGMENTS.map((s, i) => (\n            <button\n              key={s}\n              type=\"button\"\n              aria-label={`${s} layout`}\n              aria-pressed={i === seg}\n              onClick={() => selectSeg(i)}\n              className={`relative z-10 flex h-6 items-center justify-center transition-colors duration-200\n                ${i === seg ? \"text-highlight\" : \"text-ink-3\"}`}\n            >\n              <SegmentIcon kind={s} />\n            </button>\n          ))}\n        </div>\n        {chunk(fields, 2).map((pair, ri) => (\n          <div key={ri} className=\"grid min-w-0 grid-cols-2 gap-2\">\n            {pair.map((f) => (\n              <ScrubField\n                key={f.key}\n                label={f.label}\n                value={values[f.key]}\n                onChange={(v) => 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          </div>\n        ))}\n      </div>\n\n      {/* interaction section */}\n      <div className=\"primitive-card-footer flex items-center justify-between\">\n        <span className=\"text-[12px] text-ink-3\">{text.type}</span>\n        <div className=\"relative -mr-0.5 w-30\">\n          <button\n            type=\"button\"\n            aria-expanded={menuOpen}\n            onClick={() => 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(--highlight)\" : undefined }}\n          >\n            <span className={`text-[12px] ${typeValue !== text.placeholder ? \"text-ink\" : \"text-ink-3\"}`}>\n              {typeValue}\n            </span>\n            <svg\n              width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"var(--ink-3)\" strokeWidth=\"2.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\"\n              className=\"transition-transform duration-200\"\n              style={{ transform: menuOpen ? \"rotate(180deg)\" : \"rotate(0)\" }}\n            >\n              <path d=\"M6 9l6 6 6-6\" />\n            </svg>\n          </button>\n\n          {menuOpen && (\n            <div\n              className=\"absolute right-0 bottom-8 z-10 w-30 rounded-[10px] bg-card p-1 shadow-raised\"\n              style={{\n                animation: \"pop-in 200ms cubic-bezier(0.23,1,0.32,1) both\",\n                transformOrigin: \"bottom right\",\n              }}\n            >\n              <GlideMenu className=\"flex flex-col gap-px\" highlightClassName=\"inset-x-0 rounded-[6px] bg-field\">\n                {options.map((item) => (\n                  <button\n                    key={item}\n                    data-menu-row\n                    type=\"button\"\n                    onClick={() => 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                  </button>\n                ))}\n              </GlideMenu>\n            </div>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n}\n","type":"registry:component","target":"components/ward/FineTuneCard.tsx"}],"meta":{"variants":["Default"],"version":"1.1.0","source":"https://github.com/slev12397/beautiful-ui/blob/44a274e598395ab61e7c96c26fda2758780253b7/components/primitives/FineTuneCard.tsx","access":"free"},"categories":["ai"],"type":"registry:component"}