{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "resizable",
  "title": "UI Resizable",
  "description": "Two panels split by a draggable handle, where the split ratio is a numeric channel plus a handle idle/hover/press channel (dual-channel value-channel deviation). The Resizable renderer is pure (clamped 0–1 ratio, handle scale, grab-ring opacity); useResizableTransition reads the frame and eases both channels — ratio as a numeric lerp, the handle visual from its state presets. Horizontal and vertical splits; 1px divider + centered grip pill with a derived grab ring.",
  "dependencies": ["remotion"],
  "registryDependencies": ["@remocn/remocn-ui"],
  "files": [
    {
      "path": "registry/remocn-ui/resizable/index.tsx",
      "content": "\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport { mixOklch, type RemocnTheme, useRemocnTheme } from \"@/lib/remocn-ui\";\n\nexport type ResizableHandleState = \"idle\" | \"hover\" | \"press\";\n\nexport type ResizableDirection = \"horizontal\" | \"vertical\";\n\nexport interface ResizableStyle {\n  ratio: number;\n  handleScale: number;\n  handleRingOpacity: number;\n}\n\nexport interface ResizableProps {\n  first?: ReactNode;\n  second?: ReactNode;\n  direction?: ResizableDirection;\n  ratio?: number;\n  handleState?: ResizableHandleState;\n  style?: ResizableStyle;\n  minRatio?: number;\n  maxRatio?: number;\n  width?: number;\n  height?: number;\n  theme?: Partial<RemocnTheme>;\n  className?: string;\n}\n\nconst DIVIDER = 1;\nconst GRIP_LONG = 24;\nconst GRIP_SHORT = 4;\nconst RING_WIDTH = 4;\n\nfunction clampRatio(ratio: number, minRatio: number, maxRatio: number): number {\n  return Math.min(maxRatio, Math.max(minRatio, ratio));\n}\n\nexport function resizableHandleStyle(handleState: ResizableHandleState): {\n  handleScale: number;\n  handleRingOpacity: number;\n} {\n  switch (handleState) {\n    case \"hover\":\n      return { handleScale: 1.15, handleRingOpacity: 1 };\n    case \"press\":\n      return { handleScale: 1.25, handleRingOpacity: 1 };\n    default:\n      return { handleScale: 1, handleRingOpacity: 0 };\n  }\n}\n\nexport interface ResizableStyleContext {\n  containerBg: string;\n  border: string;\n  panelBg: string;\n  grip: string;\n  ring: string;\n  placeholderFg: string;\n  radius: number;\n}\n\nexport function resizableStyleContext(\n  theme: RemocnTheme,\n): ResizableStyleContext {\n  return {\n    containerBg: theme.background,\n    border: theme.border,\n    panelBg: theme.muted,\n    grip: theme.border,\n    ring: mixOklch(theme.ring, theme.background, 0.6),\n    placeholderFg: theme.mutedForeground,\n    radius: theme.radius,\n  };\n}\n\nexport function resizableStyle(\n  ratio: number,\n  handleState: ResizableHandleState,\n): ResizableStyle {\n  const handle = resizableHandleStyle(handleState);\n  return {\n    ratio,\n    handleScale: handle.handleScale,\n    handleRingOpacity: handle.handleRingOpacity,\n  };\n}\n\nfunction Placeholder({ label, color }: { label: string; color: string }) {\n  return (\n    <div\n      style={{\n        width: \"100%\",\n        height: \"100%\",\n        display: \"flex\",\n        alignItems: \"center\",\n        justifyContent: \"center\",\n        fontSize: 13,\n        fontWeight: 500,\n        letterSpacing: \"-0.01em\",\n        color,\n      }}\n    >\n      {label}\n    </div>\n  );\n}\n\nexport function Resizable({\n  first,\n  second,\n  direction = \"horizontal\",\n  ratio = 0.5,\n  handleState = \"idle\",\n  style,\n  minRatio = 0.15,\n  maxRatio = 0.85,\n  width = 440,\n  height = 240,\n  theme: themeOverride,\n  className,\n}: ResizableProps) {\n  const theme = useRemocnTheme(themeOverride, \"light\");\n  const ctx = resizableStyleContext(theme);\n\n  const v = style ?? resizableStyle(ratio, handleState);\n  const pct = clampRatio(v.ratio, minRatio, maxRatio) * 100;\n  const isHorizontal = direction === \"horizontal\";\n\n  const gripW = isHorizontal ? GRIP_SHORT : GRIP_LONG;\n  const gripH = isHorizontal ? GRIP_LONG : GRIP_SHORT;\n\n  return (\n    <div\n      className={className}\n      style={{\n        position: \"absolute\",\n        inset: 0,\n        display: \"flex\",\n        alignItems: \"center\",\n        justifyContent: \"center\",\n        background: \"transparent\",\n        fontFamily:\n          \"var(--font-geist-sans), -apple-system, BlinkMacSystemFont, sans-serif\",\n      }}\n    >\n      <div\n        style={{\n          position: \"relative\",\n          width,\n          height,\n          display: \"flex\",\n          flexDirection: isHorizontal ? \"row\" : \"column\",\n          background: ctx.containerBg,\n          border: `1px solid ${ctx.border}`,\n          borderRadius: ctx.radius,\n          overflow: \"hidden\",\n          boxSizing: \"border-box\",\n        }}\n      >\n        {}\n        <div\n          style={{\n            flex: \"none\",\n            [isHorizontal ? \"width\" : \"height\"]: `${pct}%`,\n            background: ctx.panelBg,\n            overflow: \"hidden\",\n          }}\n        >\n          {first ?? <Placeholder label=\"Panel one\" color={ctx.placeholderFg} />}\n        </div>\n        {}\n        <div\n          style={{\n            position: \"relative\",\n            flex: \"none\",\n            [isHorizontal ? \"width\" : \"height\"]: DIVIDER,\n            background: ctx.border,\n          }}\n        >\n          {}\n          <div\n            style={{\n              position: \"absolute\",\n              left: \"50%\",\n              top: \"50%\",\n              width: gripW + RING_WIDTH * 2,\n              height: gripH + RING_WIDTH * 2,\n              transform: `translate(-50%, -50%) scale(${v.handleScale})`,\n              borderRadius: 999,\n              background: ctx.ring,\n              opacity: v.handleRingOpacity,\n            }}\n          />\n          {}\n          <div\n            style={{\n              position: \"absolute\",\n              left: \"50%\",\n              top: \"50%\",\n              width: gripW,\n              height: gripH,\n              transform: `translate(-50%, -50%) scale(${v.handleScale})`,\n              borderRadius: 999,\n              background: ctx.grip,\n            }}\n          />\n        </div>\n        {}\n        <div\n          style={{\n            flex: \"1 1 0\",\n            background: ctx.panelBg,\n            overflow: \"hidden\",\n          }}\n        >\n          {second ?? (\n            <Placeholder label=\"Panel two\" color={ctx.placeholderFg} />\n          )}\n        </div>\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/resizable.tsx"
    },
    {
      "path": "registry/remocn-ui/resizable/use-resizable-transition.ts",
      "content": "\"use client\";\n\nimport { useCurrentFrame } from \"remotion\";\nimport {\n  type ResizableHandleState,\n  type ResizableStyle,\n  resizableHandleStyle,\n} from \"@/components/remocn/resizable\";\nimport { clamp01, type EasingName, easings } from \"@/lib/remocn-ui\";\n\nexport interface ResizableStep {\n  at: number;\n  ratio?: number;\n  handleState?: ResizableHandleState;\n  duration?: number;\n  easing?: EasingName;\n}\n\nexport const DEFAULT_DURATION = 18;\n\nexport interface ResizableTransitionOptions {\n  speed?: number;\n  defaultDuration?: number;\n}\n\nexport function tweenResizableStyle(\n  a: ResizableStyle,\n  b: ResizableStyle,\n  t: number,\n): ResizableStyle {\n  return {\n    ratio: a.ratio + (b.ratio - a.ratio) * t,\n    handleScale: a.handleScale + (b.handleScale - a.handleScale) * t,\n    handleRingOpacity:\n      a.handleRingOpacity + (b.handleRingOpacity - a.handleRingOpacity) * t,\n  };\n}\n\nexport function useResizableTransition(\n  steps: ResizableStep[],\n  opts: ResizableTransitionOptions = {},\n): ResizableStyle {\n  const { speed = 1 } = opts;\n  const raw = useCurrentFrame() * speed;\n  return resizableStyleAt(steps, raw, opts);\n}\n\nfunction ratioAt(\n  steps: ResizableStep[],\n  raw: number,\n  defaultDuration: number,\n): number {\n  const ratioSteps = steps.filter(\n    (s): s is ResizableStep & { ratio: number } => s.ratio !== undefined,\n  );\n  if (ratioSteps.length === 0) return 0.5;\n  const first = ratioSteps[0];\n  if (raw <= first.at) return first.ratio;\n\n  let toIndex = ratioSteps.length - 1;\n  for (let i = 1; i < ratioSteps.length; i++) {\n    if (ratioSteps[i].at > raw) {\n      toIndex = i;\n      break;\n    }\n  }\n  const pastLast = raw >= ratioSteps[ratioSteps.length - 1].at;\n  const to = pastLast ? ratioSteps[ratioSteps.length - 1] : ratioSteps[toIndex];\n  const from = pastLast\n    ? ratioSteps[ratioSteps.length - 1]\n    : ratioSteps[toIndex - 1];\n\n  const dur = to.duration ?? defaultDuration;\n  const ease = easings[to.easing ?? \"out\"];\n  const start = to.at - dur;\n  const t = pastLast || dur <= 0 ? 1 : ease(clamp01((raw - start) / dur));\n  return from.ratio + (to.ratio - from.ratio) * t;\n}\n\nfunction handleAt(\n  steps: ResizableStep[],\n  raw: number,\n  defaultDuration: number,\n): { handleScale: number; handleRingOpacity: number } {\n  const handleSteps = steps.filter(\n    (s): s is ResizableStep & { handleState: ResizableHandleState } =>\n      s.handleState !== undefined,\n  );\n  if (handleSteps.length === 0) return resizableHandleStyle(\"idle\");\n  const first = handleSteps[0];\n  if (raw <= first.at) return resizableHandleStyle(first.handleState);\n\n  let toIndex = handleSteps.length - 1;\n  for (let i = 1; i < handleSteps.length; i++) {\n    if (handleSteps[i].at > raw) {\n      toIndex = i;\n      break;\n    }\n  }\n  const pastLast = raw >= handleSteps[handleSteps.length - 1].at;\n  const to = pastLast\n    ? handleSteps[handleSteps.length - 1]\n    : handleSteps[toIndex];\n  const from = pastLast\n    ? handleSteps[handleSteps.length - 1]\n    : handleSteps[toIndex - 1];\n\n  const dur = to.duration ?? defaultDuration;\n  const ease = easings[to.easing ?? \"out\"];\n  const start = to.at - dur;\n  const t = pastLast || dur <= 0 ? 1 : ease(clamp01((raw - start) / dur));\n\n  const a = resizableHandleStyle(from.handleState);\n  const b = resizableHandleStyle(to.handleState);\n  return {\n    handleScale: a.handleScale + (b.handleScale - a.handleScale) * t,\n    handleRingOpacity:\n      a.handleRingOpacity + (b.handleRingOpacity - a.handleRingOpacity) * t,\n  };\n}\n\nexport function resizableStyleAt(\n  steps: ResizableStep[],\n  raw: number,\n  opts: ResizableTransitionOptions = {},\n): ResizableStyle {\n  const { defaultDuration = DEFAULT_DURATION } = opts;\n  const ratio = ratioAt(steps, raw, defaultDuration);\n  const handle = handleAt(steps, raw, defaultDuration);\n  return {\n    ratio,\n    handleScale: handle.handleScale,\n    handleRingOpacity: handle.handleRingOpacity,\n  };\n}\n",
      "type": "registry:component",
      "target": "components/remocn/use-resizable-transition.ts"
    }
  ],
  "type": "registry:component"
}
