{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "theme-studio",
  "type": "registry:component",
  "title": "Theme Studio",
  "description": "A live theme customiser: pick an accent hue, drag the corner radius, set the density, watch a sample card restyle as you go, then copy the CSS variables.",
  "categories": [
    "component",
    "Workspace"
  ],
  "dependencies": [
    "@base-ui/react",
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://coss.com/ui/r/slider.json"
  ],
  "files": [
    {
      "path": "registry/devl/theme-studio.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { Slider as SliderPrimitive } from \"@base-ui/react/slider\";\nimport { CheckIcon, CopyIcon, RotateCcwIcon, SparklesIcon } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { type CSSProperties, type ReactNode, useEffect, useId, useRef, useState } from \"react\";\nimport { Slider } from \"@/components/ui/slider\";\nimport { cn } from \"@/lib/utils\";\n\nexport type Density = \"compact\" | \"cozy\" | \"roomy\";\n\nexport interface ThemeValue {\n  hue: number;\n  radius: number;\n  density: Density;\n}\n\nexport interface ThemePreset {\n  name: string;\n  hue: number;\n}\n\nexport interface ThemeStudioProps {\n  defaultValue?: Partial<ThemeValue>;\n  presets?: ThemePreset[];\n  onChange?: (value: ThemeValue) => void;\n  preview?: ReactNode;\n  className?: string;\n}\n\nconst DENSITY: Record<Density, number> = { compact: 0.8, cozy: 1, roomy: 1.25 };\n\nconst DEFAULT_PRESETS: ThemePreset[] = [\n  { name: \"Indigo\", hue: 268 },\n  { name: \"Violet\", hue: 300 },\n  { name: \"Rose\", hue: 12 },\n  { name: \"Amber\", hue: 62 },\n  { name: \"Emerald\", hue: 158 },\n  { name: \"Sky\", hue: 230 },\n];\n\n// One chroma/lightness pair per theme keeps every hue legible against white text and dark surfaces.\nconst accent = (hue: number, dark = false) => `oklch(${dark ? 0.7 : 0.56} ${dark ? 0.15 : 0.18} ${Math.round(hue)})`;\n\nexport function toCss({ hue, radius, density }: ThemeValue) {\n  return `:root {\n  --primary: ${accent(hue)};\n  --primary-foreground: oklch(0.99 0 0);\n  --radius: ${(radius / 16).toFixed(3).replace(/\\.?0+$/, \"\")}rem;\n  --spacing: ${(0.25 * DENSITY[density]).toFixed(4).replace(/\\.?0+$/, \"\")}rem;\n}\n\n.dark {\n  --primary: ${accent(hue, true)};\n  --primary-foreground: oklch(0.18 0 0);\n}`;\n}\n\nexport function ThemeStudio({ defaultValue, presets = DEFAULT_PRESETS, onChange, preview, className }: ThemeStudioProps) {\n  const reduce = useReducedMotion();\n  const [initial] = useState<ThemeValue>(() => ({ hue: 268, radius: 10, density: \"cozy\", ...defaultValue }));\n  const [value, setValue] = useState<ThemeValue>(initial);\n  const [view, setView] = useState<\"preview\" | \"css\">(\"preview\");\n  const [copied, setCopied] = useState(false);\n  const [live, setLive] = useState(false);\n  const copyTimer = useRef<number>(undefined);\n  useEffect(() => () => clearTimeout(copyTimer.current), []);\n\n  const update = (patch: Partial<ThemeValue>) =>\n    setValue((v) => {\n      const next = { ...v, ...patch };\n      onChange?.(next);\n      return next;\n    });\n\n  const copy = async () => {\n    try {\n      await navigator.clipboard.writeText(toCss(value));\n    } catch {}\n    setCopied(true);\n    clearTimeout(copyTimer.current);\n    copyTimer.current = window.setTimeout(() => setCopied(false), 1600);\n  };\n\n  const vars = {\n    \"--studio-hue\": value.hue,\n    \"--studio-radius\": `${value.radius}px`,\n    \"--studio-density\": DENSITY[value.density],\n  } as CSSProperties;\n\n  const activePreset = presets.find((p) => Math.abs(p.hue - value.hue) < 2);\n\n  return (\n    <div\n      className={cn(\n        \"grid w-full max-w-[640px] overflow-hidden rounded-2xl border bg-card text-card-foreground shadow-xs/5 sm:grid-cols-[240px_1fr]\",\n        className,\n      )}\n    >\n      <div className=\"flex flex-col gap-5 border-b p-4 sm:border-e sm:border-b-0\">\n        <Field label=\"Accent\" value={activePreset?.name ?? `${Math.round(value.hue)}°`}>\n          <div aria-label=\"Accent presets\" className=\"grid w-fit grid-cols-6 gap-1.5\" role=\"radiogroup\">\n            {presets.map((p) => {\n              const on = activePreset?.name === p.name;\n              return (\n                <button\n                  aria-checked={on}\n                  aria-label={p.name}\n                  className=\"relative flex size-7 cursor-pointer items-center justify-center rounded-full outline-none transition-transform duration-150 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-card active:scale-[0.95] before:absolute before:-inset-0.5 before:content-['']\"\n                  key={p.name}\n                  onClick={() => update({ hue: p.hue })}\n                  role=\"radio\"\n                  type=\"button\"\n                >\n                  <span\n                    className=\"absolute inset-0 rounded-full shadow-[inset_0_0_0_1px_--theme(--color-black/10%)] [background:var(--swatch)] dark:[background:var(--swatch-dark)]\"\n                    style={{ \"--swatch\": accent(p.hue), \"--swatch-dark\": accent(p.hue, true) } as CSSProperties}\n                  />\n                  <AnimatePresence initial={false}>\n                    {on && (\n                      <motion.span\n                        animate={{ opacity: 1, scale: 1 }}\n                        className=\"relative text-white dark:text-neutral-900\"\n                        exit={{ opacity: 0, scale: 0.6 }}\n                        initial={reduce ? false : { opacity: 0, scale: 0.6 }}\n                        transition={{ duration: 0.15, ease: \"easeOut\" }}\n                      >\n                        <CheckIcon className=\"size-3.5\" strokeWidth={3} />\n                      </motion.span>\n                    )}\n                  </AnimatePresence>\n                </button>\n              );\n            })}\n          </div>\n          <SliderPrimitive.Root\n            className=\"mt-3 w-full\"\n            max={360}\n            min={0}\n            onValueChange={(v) => {\n              setLive(true);\n              update({ hue: Array.isArray(v) ? v[0] : v });\n            }}\n            onValueCommitted={() => setLive(false)}\n            thumbAlignment=\"edge\"\n            value={value.hue}\n          >\n            <SliderPrimitive.Control className=\"flex h-5 w-full touch-none select-none items-center\">\n              <SliderPrimitive.Track\n                className=\"relative h-2.5 w-full rounded-full shadow-[inset_0_0_0_1px_--theme(--color-black/8%)]\"\n                style={{\n                  background: `linear-gradient(to right, ${Array.from({ length: 13 }, (_, i) => accent(i * 30)).join(\", \")})`,\n                }}\n              >\n                <SliderPrimitive.Thumb\n                  getAriaLabel={() => \"Accent hue\"}\n                  className=\"block size-4.5 rounded-full border-2 border-white shadow-[0_1px_3px_--theme(--color-black/30%)] outline-none transition-[scale] duration-150 has-focus-visible:ring-2 has-focus-visible:ring-ring has-focus-visible:ring-offset-2 has-focus-visible:ring-offset-card data-dragging:scale-110\"\n                  style={{ background: accent(value.hue) }}\n                />\n              </SliderPrimitive.Track>\n            </SliderPrimitive.Control>\n          </SliderPrimitive.Root>\n        </Field>\n\n        <Field label=\"Radius\" value={`${value.radius}px`}>\n          <Slider\n            className=\"py-1.5\"\n            getAriaLabel={() => \"Corner radius\"}\n            max={20}\n            min={0}\n            onValueChange={(v) => {\n              setLive(true);\n              update({ radius: Array.isArray(v) ? v[0] : v });\n            }}\n            onValueCommitted={() => setLive(false)}\n            value={value.radius}\n          />\n        </Field>\n\n        <Field label=\"Density\">\n          <Segmented\n            label=\"Density\"\n            onChange={(d) => update({ density: d })}\n            options={[\n              { value: \"compact\", label: \"Compact\" },\n              { value: \"cozy\", label: \"Cozy\" },\n              { value: \"roomy\", label: \"Roomy\" },\n            ]}\n            value={value.density}\n          />\n        </Field>\n      </div>\n\n      <div className=\"flex min-w-0 flex-col bg-muted/40\">\n        <div className=\"flex items-center gap-1 px-3 pt-3\">\n          <Segmented\n            label=\"View\"\n            onChange={setView}\n            options={[\n              { value: \"preview\", label: \"Preview\" },\n              { value: \"css\", label: \"CSS\" },\n            ]}\n            value={view}\n          />\n          <button\n            className=\"ms-auto flex h-7 cursor-pointer items-center gap-1.5 rounded-md px-2 font-medium text-muted-foreground text-xs outline-none transition-[color,background-color,opacity,scale] duration-150 hover:bg-accent hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring active:scale-[0.97] disabled:pointer-events-none disabled:opacity-0\"\n            disabled={value.hue === initial.hue && value.radius === initial.radius && value.density === initial.density}\n            onClick={() => update(initial)}\n            type=\"button\"\n          >\n            <RotateCcwIcon className=\"size-3.5\" />\n            Reset\n          </button>\n          <button\n            className=\"flex h-7 cursor-pointer items-center gap-1.5 rounded-md px-2 font-medium text-muted-foreground text-xs outline-none transition-[color,background-color,scale] duration-150 hover:bg-accent hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring active:scale-[0.97]\"\n            onClick={copy}\n            type=\"button\"\n          >\n            <span className=\"relative size-3.5\">\n              <AnimatePresence initial={false}>\n                <motion.span\n                  animate={{ opacity: 1, scale: 1, filter: \"blur(0px)\" }}\n                  className=\"absolute inset-0\"\n                  exit={{ opacity: 0, scale: 0.6, filter: \"blur(2px)\" }}\n                  initial={reduce ? false : { opacity: 0, scale: 0.6, filter: \"blur(2px)\" }}\n                  key={copied ? \"done\" : \"copy\"}\n                  transition={{ duration: 0.16, ease: \"easeOut\" }}\n                >\n                  {copied ? <CheckIcon className=\"size-3.5 text-success\" /> : <CopyIcon className=\"size-3.5\" />}\n                </motion.span>\n              </AnimatePresence>\n            </span>\n            <span className=\"grid text-start *:col-start-1 *:row-start-1\">\n              <span className={cn(copied && \"invisible\")}>Copy CSS</span>\n              <span className={cn(!copied && \"invisible\")}>Copied</span>\n            </span>\n          </button>\n        </div>\n        <div className={cn(\"relative min-h-[324px] flex-1\", live && \"[&_*]:transition-none!\")} style={vars}>\n          <AnimatePresence initial={false} mode=\"popLayout\">\n            {view === \"preview\" ? (\n              <motion.div\n                animate={{ opacity: 1 }}\n                className=\"flex h-full items-center justify-center p-4\"\n                exit={{ opacity: 0 }}\n                initial={{ opacity: 0 }}\n                key=\"preview\"\n                transition={{ duration: 0.15 }}\n              >\n                {preview ?? <SamplePreview />}\n              </motion.div>\n            ) : (\n              <motion.pre\n                animate={{ opacity: 1 }}\n                className=\"m-3 overflow-x-auto rounded-xl border bg-background p-3 font-mono text-[11.5px] text-muted-foreground leading-relaxed\"\n                exit={{ opacity: 0 }}\n                initial={{ opacity: 0 }}\n                key=\"css\"\n                transition={{ duration: 0.15 }}\n              >\n                <CssCode css={toCss(value)} />\n              </motion.pre>\n            )}\n          </AnimatePresence>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction CssCode({ css }: { css: string }) {\n  return (\n    <code>\n      {css.split(\"\\n\").map((line, i) => {\n        const m = line.match(/^(\\s*)(--[\\w-]+)(:\\s*)(.+)(;)$/);\n        return (\n          <span className=\"block\" key={i}>\n            {m ? (\n              <>\n                {m[1]}\n                <span className=\"text-sky-700 dark:text-sky-300\">{m[2]}</span>\n                {m[3]}\n                <span className=\"text-foreground\">{m[4]}</span>\n                {m[5]}\n              </>\n            ) : (\n              line || \" \"\n            )}\n          </span>\n        );\n      })}\n    </code>\n  );\n}\n\nfunction Field({ label, value, children }: { label: string; value?: string; children: ReactNode }) {\n  return (\n    <div>\n      <div className=\"mb-2 flex items-baseline justify-between\">\n        <span className=\"font-medium text-xs\">{label}</span>\n        {value && <span className=\"font-mono text-[11px] text-muted-foreground tabular-nums\">{value}</span>}\n      </div>\n      {children}\n    </div>\n  );\n}\n\nfunction Segmented<T extends string>({\n  value,\n  onChange,\n  options,\n  label,\n}: {\n  value: T;\n  onChange: (v: T) => void;\n  options: { value: T; label: string }[];\n  label: string;\n}) {\n  const id = useId();\n  return (\n    <div aria-label={label} className=\"flex w-fit rounded-lg bg-muted p-0.5\" role=\"radiogroup\">\n      {options.map((o) => (\n        <button\n          aria-checked={value === o.value}\n          className={cn(\n            \"relative z-0 h-7 cursor-pointer rounded-md px-2.5 font-medium text-xs outline-none transition-colors duration-150 focus-visible:ring-2 focus-visible:ring-ring\",\n            value === o.value ? \"text-foreground\" : \"text-muted-foreground hover:text-foreground\",\n          )}\n          key={o.value}\n          onClick={() => onChange(o.value)}\n          role=\"radio\"\n          type=\"button\"\n        >\n          {value === o.value && (\n            <motion.span\n              className=\"absolute inset-0 -z-10 rounded-md bg-background shadow-sm/5 dark:bg-input\"\n              layoutId={`${id}-seg`}\n              transition={{ type: \"spring\", stiffness: 500, damping: 40 }}\n            />\n          )}\n          {o.label}\n        </button>\n      ))}\n    </div>\n  );\n}\n\nfunction SamplePreview() {\n  const [on, setOn] = useState(true);\n  const pad = \"calc(var(--studio-density) * 1rem)\";\n  const gap = \"calc(var(--studio-density) * 0.75rem)\";\n  return (\n    <div\n      className=\"w-full max-w-[300px] border bg-card shadow-xs/5 transition-[border-radius,padding] duration-200 ease-out [--studio-accent:oklch(0.56_0.18_var(--studio-hue))] [--studio-ink:oklch(0.48_0.16_var(--studio-hue))] [--studio-on:oklch(0.99_0_0)] dark:[--studio-accent:oklch(0.7_0.15_var(--studio-hue))] dark:[--studio-ink:oklch(0.8_0.12_var(--studio-hue))] dark:[--studio-on:oklch(0.18_0_0)]\"\n      style={{ borderRadius: \"calc(var(--studio-radius) + 6px)\", padding: pad }}\n    >\n      <div className=\"flex flex-col transition-[gap] duration-200\" style={{ gap }}>\n        <div className=\"flex items-center gap-2.5\">\n          <span\n            className=\"flex size-9 items-center justify-center bg-[color-mix(in_oklch,var(--studio-accent)_16%,transparent)] text-(--studio-accent) transition-[border-radius,color,background-color] duration-200\"\n            style={{ borderRadius: \"var(--studio-radius)\" }}\n          >\n            <SparklesIcon className=\"size-4\" />\n          </span>\n          <div className=\"min-w-0 flex-1\">\n            <p className=\"font-medium text-sm\">Brightwater Studio</p>\n            <p className=\"text-muted-foreground text-xs\">Team plan · 8 seats</p>\n          </div>\n          <span\n            className=\"bg-[color-mix(in_oklch,var(--studio-accent)_14%,transparent)] px-2 py-0.5 font-medium text-(--studio-ink) text-[11px] transition-[border-radius,color,background-color] duration-200\"\n            style={{ borderRadius: \"calc(var(--studio-radius) * 0.6)\" }}\n          >\n            Pro\n          </span>\n        </div>\n        <label className=\"flex flex-col gap-1.5\">\n          <span className=\"font-medium text-xs\">Workspace URL</span>\n          <span\n            className=\"flex h-8 items-center border bg-background px-2.5 text-sm ring-(--studio-accent)/30 transition-[border-radius] duration-200 focus-within:border-(--studio-accent) focus-within:ring-3\"\n            style={{ borderRadius: \"var(--studio-radius)\" }}\n          >\n            <span className=\"text-muted-foreground\">halden.app/</span>\n            <input aria-label=\"Workspace URL\" className=\"min-w-0 flex-1 bg-transparent outline-none\" defaultValue=\"brightwater\" />\n          </span>\n        </label>\n        <div className=\"flex items-center justify-between gap-3\">\n          <div>\n            <p className=\"font-medium text-xs\">Weekly digest</p>\n            <p className=\"text-muted-foreground text-xs\">Mondays at 9:00</p>\n          </div>\n          <button\n            aria-checked={on}\n            aria-label=\"Weekly digest\"\n            className={cn(\n              \"flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full p-0.5 outline-none transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-ring\",\n              on ? \"bg-(--studio-accent)\" : \"bg-input\",\n            )}\n            onClick={() => setOn((v) => !v)}\n            role=\"switch\"\n            type=\"button\"\n          >\n            <span className={cn(\"size-4 rounded-full bg-white shadow-xs transition-transform duration-200 ease-out\", on && \"translate-x-4\")} />\n          </button>\n        </div>\n        <div>\n          <div className=\"mb-1.5 flex justify-between text-xs\">\n            <span className=\"text-muted-foreground\">Storage</span>\n            <span className=\"text-muted-foreground tabular-nums\">68%</span>\n          </div>\n          <div className=\"h-1.5 overflow-hidden rounded-full bg-muted\">\n            <div className=\"h-full w-[68%] rounded-full bg-(--studio-accent) transition-colors duration-200\" />\n          </div>\n        </div>\n        <div className=\"flex justify-end gap-2\">\n          <button\n            className=\"h-8 cursor-pointer border bg-background px-3 font-medium text-sm outline-none transition-[border-radius,scale] duration-200 hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring active:scale-[0.97]\"\n            style={{ borderRadius: \"var(--studio-radius)\" }}\n            type=\"button\"\n          >\n            Cancel\n          </button>\n          <button\n            className=\"h-8 cursor-pointer bg-(--studio-accent) px-3 font-medium text-(--studio-on) text-sm shadow-xs outline-none transition-[border-radius,background-color,scale] duration-200 hover:brightness-105 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 active:scale-[0.97]\"\n            style={{ borderRadius: \"var(--studio-radius)\" }}\n            type=\"button\"\n          >\n            Save changes\n          </button>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
    }
  ]
}