{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "button",
  "title": "UI Button",
  "description": "A button whose idle/hover/press/loading/success state is a pure function of the timeline; the loading state composes the Spinner motion atom.",
  "dependencies": ["remotion"],
  "registryDependencies": ["@remocn/remocn-ui", "@remocn/spinner"],
  "files": [
    {
      "path": "registry/remocn-ui/button/index.tsx",
      "content": "\"use client\";\n\nimport { Spinner } from \"@/components/remocn/spinner\";\nimport { mixOklch, type RemocnTheme, useRemocnTheme } from \"@/lib/remocn-ui\";\n\nexport type ButtonState = \"idle\" | \"hover\" | \"press\" | \"loading\" | \"success\";\n\ntype ButtonVariant =\n  | \"default\"\n  | \"secondary\"\n  | \"destructive\"\n  | \"outline\"\n  | \"ghost\";\n\ntype ButtonSize = \"sm\" | \"default\" | \"lg\";\n\nexport interface ButtonProps {\n  state?: ButtonState;\n  style?: ButtonStyle;\n  label?: string;\n  variant?: ButtonVariant;\n  size?: ButtonSize;\n  theme?: Partial<RemocnTheme>;\n  primary?: string;\n  speed?: number;\n  align?: \"start\" | \"center\" | \"end\";\n  className?: string;\n}\n\nfunction justify(align: \"start\" | \"center\" | \"end\"): string {\n  return align === \"start\"\n    ? \"flex-start\"\n    : align === \"end\"\n      ? \"flex-end\"\n      : \"center\";\n}\n\nconst CHECK_PATH_LENGTH = 14;\n\nconst SIZE_STYLES: Record<\n  ButtonSize,\n  { height: number; padding: string; fontSize: number; gap: number }\n> = {\n  sm: { height: 32, padding: \"0 12px\", fontSize: 13, gap: 6 },\n  default: { height: 40, padding: \"0 20px\", fontSize: 15, gap: 8 },\n  lg: { height: 48, padding: \"0 28px\", fontSize: 17, gap: 10 },\n};\n\ninterface VariantTokens {\n  bg: string;\n  fg: string;\n  hoverBg: string;\n  border: string;\n}\n\nfunction variantTokens(\n  variant: ButtonVariant,\n  theme: RemocnTheme,\n): VariantTokens {\n  const variants = {\n    secondary: {\n      bg: theme.secondary,\n      fg: theme.secondaryForeground,\n      hoverBg: mixOklch(theme.secondary, theme.muted, 1),\n      border: \"transparent\",\n    },\n    destructive: {\n      bg: theme.destructive,\n      fg: theme.destructiveForeground,\n      hoverBg: mixOklch(theme.destructive, theme.foreground, 0.12),\n      border: \"transparent\",\n    },\n    outline: {\n      bg: \"transparent\",\n      fg: theme.foreground,\n      hoverBg: theme.accent,\n      border: theme.border,\n    },\n    ghost: {\n      bg: \"transparent\",\n      fg: theme.foreground,\n      hoverBg: theme.accent,\n      border: \"transparent\",\n    },\n    default: {\n      bg: theme.primary,\n      fg: theme.primaryForeground,\n      hoverBg: mixOklch(theme.primary, theme.foreground, 0.1),\n      border: \"transparent\",\n    },\n  };\n\n  return variants[variant] ?? variants.default;\n}\n\nexport interface ButtonStyle {\n  translateY: number;\n  scale: number;\n  background: string;\n  labelOpacity: number;\n  spinnerOpacity: number;\n  checkOpacity: number;\n}\n\nexport interface ButtonStyleContext {\n  restBg: string;\n  hoverBg: string;\n  pressBg: string;\n  primary: string;\n}\n\nexport function buttonStyleContext(\n  variant: ButtonVariant,\n  theme: RemocnTheme,\n): ButtonStyleContext {\n  const tokens = variantTokens(variant, theme);\n  const restBg = tokens.bg === \"transparent\" ? theme.background : tokens.bg;\n  return {\n    restBg,\n    hoverBg: tokens.hoverBg,\n    pressBg:\n      tokens.bg === \"transparent\"\n        ? tokens.hoverBg\n        : mixOklch(tokens.hoverBg, theme.foreground, 0.08),\n    primary: theme.primary,\n  };\n}\n\nexport function buttonStyle(\n  state: ButtonState,\n  ctx: ButtonStyleContext,\n): ButtonStyle {\n  switch (state) {\n    case \"hover\":\n      return {\n        translateY: -1,\n        scale: 1,\n        background: ctx.hoverBg,\n        labelOpacity: 1,\n        spinnerOpacity: 0,\n        checkOpacity: 0,\n      };\n    case \"press\":\n      return {\n        translateY: -1,\n        scale: 0.97,\n        background: ctx.pressBg,\n        labelOpacity: 1,\n        spinnerOpacity: 0,\n        checkOpacity: 0,\n      };\n    case \"loading\":\n      return {\n        translateY: -1,\n        scale: 1,\n        background: ctx.hoverBg,\n        labelOpacity: 0,\n        spinnerOpacity: 1,\n        checkOpacity: 0,\n      };\n    case \"success\":\n      return {\n        translateY: -1,\n        scale: 1,\n        background: ctx.primary,\n        labelOpacity: 0,\n        spinnerOpacity: 0,\n        checkOpacity: 1,\n      };\n    default:\n      return {\n        translateY: 0,\n        scale: 1,\n        background: ctx.restBg,\n        labelOpacity: 1,\n        spinnerOpacity: 0,\n        checkOpacity: 0,\n      };\n  }\n}\n\nexport function Button({\n  state = \"idle\",\n  style,\n  label = \"Continue\",\n  variant = \"default\",\n  size = \"default\",\n  theme: themeOverride,\n  primary,\n  speed = 1,\n  align = \"center\",\n  className,\n}: ButtonProps) {\n  const theme = useRemocnTheme(\n    { ...themeOverride, ...(primary ? { primary } : {}) },\n    \"light\",\n  );\n\n  const sizeStyle = SIZE_STYLES[size];\n  const tokens = variantTokens(variant, theme);\n\n  const ctx = buttonStyleContext(variant, theme);\n  const v = style ?? buttonStyle(state, ctx);\n  const iconSize = Math.round(sizeStyle.fontSize * 1.1);\n\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        inset: 0,\n        display: \"flex\",\n        alignItems: \"center\",\n        justifyContent: justify(align),\n        background: \"transparent\",\n        fontFamily:\n          \"var(--font-geist-sans), -apple-system, BlinkMacSystemFont, sans-serif\",\n      }}\n    >\n      <button\n        type=\"button\"\n        className={className}\n        style={{\n          position: \"relative\",\n          transform: `translateY(${v.translateY}px) scale(${v.scale})`,\n          display: \"inline-flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          gap: sizeStyle.gap,\n          height: sizeStyle.height,\n          padding: sizeStyle.padding,\n          fontSize: sizeStyle.fontSize,\n          fontWeight: 500,\n          letterSpacing: \"-0.01em\",\n          color: tokens.fg,\n          background: v.background,\n          border:\n            variant === \"outline\"\n              ? `1px solid ${tokens.border}`\n              : \"1px solid transparent\",\n          borderRadius: theme.radius,\n          cursor: \"pointer\",\n        }}\n      >\n        {}\n        <span style={{ position: \"relative\", display: \"inline-flex\" }}>\n          <span style={{ opacity: v.labelOpacity }}>{label}</span>\n          <span\n            style={{\n              position: \"absolute\",\n              inset: 0,\n              display: \"flex\",\n              alignItems: \"center\",\n              justifyContent: \"center\",\n              opacity: v.spinnerOpacity,\n            }}\n          >\n            <Spinner color={tokens.fg} speed={speed} size={iconSize} />\n          </span>\n          <span\n            style={{\n              position: \"absolute\",\n              inset: 0,\n              display: \"flex\",\n              alignItems: \"center\",\n              justifyContent: \"center\",\n              opacity: v.checkOpacity,\n            }}\n          >\n            <svg\n              width={iconSize}\n              height={iconSize}\n              viewBox=\"0 0 24 24\"\n              fill=\"none\"\n            >\n              <path\n                d=\"M5 12.5l4.5 4.5L19 7\"\n                stroke={tokens.fg}\n                strokeWidth=\"2.6\"\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n                strokeDasharray={CHECK_PATH_LENGTH}\n                strokeDashoffset={0}\n                pathLength={CHECK_PATH_LENGTH}\n              />\n            </svg>\n          </span>\n        </span>\n      </button>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/button.tsx"
    },
    {
      "path": "registry/remocn-ui/button/use-button-transition.ts",
      "content": "\"use client\";\n\nimport {\n  type ButtonState,\n  type ButtonStyle,\n  buttonStyle,\n  buttonStyleContext,\n} from \"@/components/remocn/button\";\nimport {\n  easings,\n  mixOklch,\n  type RemocnTheme,\n  type Step,\n  useRemocnTheme,\n  useStateTransition,\n} from \"@/lib/remocn-ui\";\n\nexport const DEFAULT_DURATION = 8;\n\nexport function tweenButtonStyle(\n  a: ButtonStyle,\n  b: ButtonStyle,\n  t: number,\n): ButtonStyle {\n  return {\n    translateY: a.translateY + (b.translateY - a.translateY) * t,\n    scale: a.scale + (b.scale - a.scale) * t,\n    labelOpacity: a.labelOpacity + (b.labelOpacity - a.labelOpacity) * t,\n    spinnerOpacity:\n      a.spinnerOpacity + (b.spinnerOpacity - a.spinnerOpacity) * t,\n    checkOpacity: a.checkOpacity + (b.checkOpacity - a.checkOpacity) * t,\n    background: mixOklch(a.background, b.background, t),\n  };\n}\n\nexport interface ButtonTransitionOptions {\n  variant?: \"default\" | \"secondary\" | \"destructive\" | \"outline\" | \"ghost\";\n  theme?: Partial<RemocnTheme>;\n  mode?: \"light\" | \"dark\";\n  primary?: string;\n  speed?: number;\n  defaultDuration?: number;\n}\n\nexport function useButtonTransition(\n  steps: Step<ButtonState>[],\n  opts: ButtonTransitionOptions = {},\n): ButtonStyle {\n  const {\n    variant = \"default\",\n    theme: themeOverride,\n    mode,\n    primary,\n    speed = 1,\n    defaultDuration = DEFAULT_DURATION,\n  } = opts;\n  const theme = useRemocnTheme(\n    { ...themeOverride, ...(primary ? { primary } : {}) },\n    mode,\n  );\n  const ctx = buttonStyleContext(variant, theme);\n  const { from, to, progress } = useStateTransition(\n    steps,\n    \"idle\",\n    speed,\n    defaultDuration,\n  );\n  const t = easings.out(progress);\n  return tweenButtonStyle(buttonStyle(from, ctx), buttonStyle(to, ctx), t);\n}\n",
      "type": "registry:component",
      "target": "components/remocn/use-button-transition.ts"
    }
  ],
  "type": "registry:component"
}
