{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "widget-controls",
  "title": "Widget Controls",
  "description": "Built-in shadcn-backed widget components for standard ipywidgets including controls (IntSlider, FloatSlider, IntRangeSlider, FloatRangeSlider, IntProgress, Button, Checkbox, Text, Textarea, HTML, ColorPicker, Dropdown, RadioButtons, ToggleButton, ToggleButtons, SelectMultiple) and layout containers (VBox, HBox, Box, GridBox, Accordion, Tab).",
  "registryDependencies": [
    "@nteract/widget-view",
    "@nteract/output-area",
    "slider",
    "progress",
    "button",
    "checkbox",
    "input",
    "textarea",
    "label",
    "select",
    "radio-group",
    "toggle",
    "toggle-group",
    "accordion",
    "tabs",
    "command",
    "popover",
    "badge"
  ],
  "files": [
    {
      "path": "registry/widgets/controls/index.ts",
      "content": "/**\n * Built-in widget components for @jupyter-widgets/controls.\n *\n * This module registers all built-in widget components with the widget registry.\n * Import this module to enable rendering of standard ipywidgets.\n */\n\nimport { registerWidget } from \"../widget-registry\";\nimport { AccordionWidget } from \"./accordion-widget\";\nimport { AudioWidget } from \"./audio-widget\";\nimport { BoundedFloatTextWidget } from \"./bounded-float-text-widget\";\nimport { BoundedIntTextWidget } from \"./bounded-int-text-widget\";\nimport { BoxWidget } from \"./box-widget\";\nimport { ButtonWidget } from \"./button-widget\";\nimport { CheckboxWidget } from \"./checkbox-widget\";\nimport { ColorPicker } from \"./color-picker\";\nimport { ColorsInputWidget } from \"./colors-input-widget\";\nimport { ComboboxWidget } from \"./combobox-widget\";\nimport { ControllerAxisWidget } from \"./controller-axis-widget\";\nimport { ControllerButtonWidget } from \"./controller-button-widget\";\nimport { ControllerWidget } from \"./controller-widget\";\nimport { DatePickerWidget } from \"./date-picker-widget\";\nimport { DatetimeWidget } from \"./datetime-widget\";\n// Selection widgets\nimport { DropdownWidget } from \"./dropdown-widget\";\nimport { FileUploadWidget } from \"./file-upload-widget\";\nimport { FloatLogSlider } from \"./float-log-slider\";\nimport { FloatProgress } from \"./float-progress\";\nimport { FloatRangeSlider } from \"./float-range-slider\";\nimport { FloatSlider } from \"./float-slider\";\nimport { FloatTextWidget } from \"./float-text-widget\";\nimport { FloatsInputWidget } from \"./floats-input-widget\";\nimport { GridBoxWidget } from \"./gridbox-widget\";\nimport { HBoxWidget } from \"./hbox-widget\";\nimport { HTMLMathWidget } from \"./html-math-widget\";\nimport { HTMLWidget } from \"./html-widget\";\nimport { ImageWidget } from \"./image-widget\";\nimport { IntProgress } from \"./int-progress\";\nimport { IntRangeSlider } from \"./int-range-slider\";\n// Import widget components\nimport { IntSlider } from \"./int-slider\";\nimport { IntTextWidget } from \"./int-text-widget\";\nimport { IntsInputWidget } from \"./ints-input-widget\";\nimport { LabelWidget } from \"./label-widget\";\nimport { DirectionalLinkWidget, LinkWidget } from \"./link-widget\";\nimport { OutputWidget } from \"./output-widget\";\nimport { PasswordWidget } from \"./password-widget\";\nimport { PlayWidget } from \"./play-widget\";\nimport { RadioButtonsWidget } from \"./radio-buttons-widget\";\nimport { SelectMultipleWidget } from \"./select-multiple-widget\";\nimport { SelectWidget } from \"./select-widget\";\nimport { SelectionRangeSliderWidget } from \"./selection-range-slider-widget\";\nimport { SelectionSliderWidget } from \"./selection-slider-widget\";\nimport { StackWidget } from \"./stack-widget\";\nimport { TabWidget } from \"./tab-widget\";\nimport { TagsInputWidget } from \"./tags-input-widget\";\nimport { TextWidget } from \"./text-widget\";\nimport { TextareaWidget } from \"./textarea-widget\";\nimport { TimePickerWidget } from \"./time-picker-widget\";\nimport { ToggleButtonWidget } from \"./toggle-button-widget\";\nimport { ToggleButtonsWidget } from \"./toggle-buttons-widget\";\nimport { ValidWidget } from \"./valid-widget\";\n// Import layout widget components\nimport { VBoxWidget } from \"./vbox-widget\";\nimport { VideoWidget } from \"./video-widget\";\n\n// Register all widgets with their model names\nregisterWidget(\"IntSliderModel\", IntSlider);\nregisterWidget(\"FloatSliderModel\", FloatSlider);\nregisterWidget(\"FloatLogSliderModel\", FloatLogSlider);\nregisterWidget(\"IntRangeSliderModel\", IntRangeSlider);\nregisterWidget(\"FloatRangeSliderModel\", FloatRangeSlider);\nregisterWidget(\"SelectionSliderModel\", SelectionSliderWidget);\nregisterWidget(\"SelectionRangeSliderModel\", SelectionRangeSliderWidget);\nregisterWidget(\"IntProgressModel\", IntProgress);\nregisterWidget(\"FloatProgressModel\", FloatProgress);\nregisterWidget(\"ButtonModel\", ButtonWidget);\nregisterWidget(\"PlayModel\", PlayWidget);\nregisterWidget(\"CheckboxModel\", CheckboxWidget);\nregisterWidget(\"TextModel\", TextWidget);\nregisterWidget(\"TextareaModel\", TextareaWidget);\nregisterWidget(\"HTMLModel\", HTMLWidget);\nregisterWidget(\"HTMLMathModel\", HTMLMathWidget);\nregisterWidget(\"LabelModel\", LabelWidget);\nregisterWidget(\"ImageModel\", ImageWidget);\nregisterWidget(\"AudioModel\", AudioWidget);\nregisterWidget(\"VideoModel\", VideoWidget);\nregisterWidget(\"ColorPickerModel\", ColorPicker);\nregisterWidget(\"DatePickerModel\", DatePickerWidget);\nregisterWidget(\"DatetimeModel\", DatetimeWidget);\nregisterWidget(\"NaiveDatetimeModel\", DatetimeWidget);\nregisterWidget(\"TimePickerModel\", TimePickerWidget);\nregisterWidget(\"TimeModel\", TimePickerWidget);\n// Numeric text inputs\nregisterWidget(\"IntTextModel\", IntTextWidget);\nregisterWidget(\"FloatTextModel\", FloatTextWidget);\nregisterWidget(\"BoundedIntTextModel\", BoundedIntTextWidget);\nregisterWidget(\"BoundedFloatTextModel\", BoundedFloatTextWidget);\nregisterWidget(\"PasswordModel\", PasswordWidget);\nregisterWidget(\"ValidModel\", ValidWidget);\n// Selection widgets\nregisterWidget(\"DropdownModel\", DropdownWidget);\nregisterWidget(\"ComboboxModel\", ComboboxWidget);\nregisterWidget(\"RadioButtonsModel\", RadioButtonsWidget);\nregisterWidget(\"SelectModel\", SelectWidget);\nregisterWidget(\"SelectMultipleModel\", SelectMultipleWidget);\nregisterWidget(\"ToggleButtonModel\", ToggleButtonWidget);\nregisterWidget(\"ToggleButtonsModel\", ToggleButtonsWidget);\n// Multi-value inputs\nregisterWidget(\"TagsInputModel\", TagsInputWidget);\nregisterWidget(\"ColorsInputModel\", ColorsInputWidget);\nregisterWidget(\"IntsInputModel\", IntsInputWidget);\nregisterWidget(\"FloatsInputModel\", FloatsInputWidget);\nregisterWidget(\"FileUploadModel\", FileUploadWidget);\n\n// Link widgets (frontend-only property synchronization)\nregisterWidget(\"LinkModel\", LinkWidget);\nregisterWidget(\"DirectionalLinkModel\", DirectionalLinkWidget);\n\n// Register layout widgets\nregisterWidget(\"VBoxModel\", VBoxWidget);\nregisterWidget(\"HBoxModel\", HBoxWidget);\nregisterWidget(\"BoxModel\", BoxWidget);\nregisterWidget(\"GridBoxModel\", GridBoxWidget);\nregisterWidget(\"AccordionModel\", AccordionWidget);\nregisterWidget(\"TabModel\", TabWidget);\nregisterWidget(\"StackModel\", StackWidget);\n\n// Controller widgets (Gamepad API)\nregisterWidget(\"ControllerModel\", ControllerWidget);\nregisterWidget(\"ControllerButtonModel\", ControllerButtonWidget);\nregisterWidget(\"ControllerAxisModel\", ControllerAxisWidget);\n\n// Output widget (@jupyter-widgets/output)\nregisterWidget(\"OutputModel\", OutputWidget);\n\nexport { AccordionWidget } from \"./accordion-widget\";\nexport { AudioWidget } from \"./audio-widget\";\nexport { BoundedFloatTextWidget } from \"./bounded-float-text-widget\";\nexport { BoundedIntTextWidget } from \"./bounded-int-text-widget\";\nexport { BoxWidget } from \"./box-widget\";\nexport { ButtonWidget } from \"./button-widget\";\nexport { CheckboxWidget } from \"./checkbox-widget\";\nexport { ColorPicker } from \"./color-picker\";\nexport { ColorsInputWidget } from \"./colors-input-widget\";\nexport { ComboboxWidget } from \"./combobox-widget\";\nexport { ControllerAxisWidget } from \"./controller-axis-widget\";\nexport { ControllerButtonWidget } from \"./controller-button-widget\";\nexport { ControllerWidget } from \"./controller-widget\";\nexport { DatePickerWidget } from \"./date-picker-widget\";\nexport { DatetimeWidget } from \"./datetime-widget\";\n// Selection widgets\nexport { DropdownWidget } from \"./dropdown-widget\";\nexport { FileUploadWidget } from \"./file-upload-widget\";\nexport { FloatLogSlider } from \"./float-log-slider\";\nexport { FloatProgress } from \"./float-progress\";\nexport { FloatRangeSlider } from \"./float-range-slider\";\nexport { FloatSlider } from \"./float-slider\";\nexport { FloatTextWidget } from \"./float-text-widget\";\nexport { FloatsInputWidget } from \"./floats-input-widget\";\nexport { GridBoxWidget } from \"./gridbox-widget\";\nexport { HBoxWidget } from \"./hbox-widget\";\nexport { HTMLMathWidget } from \"./html-math-widget\";\nexport { HTMLWidget } from \"./html-widget\";\nexport { ImageWidget } from \"./image-widget\";\nexport { IntProgress } from \"./int-progress\";\nexport { IntRangeSlider } from \"./int-range-slider\";\n// Re-export components for direct use\nexport { IntSlider } from \"./int-slider\";\nexport { IntTextWidget } from \"./int-text-widget\";\nexport { IntsInputWidget } from \"./ints-input-widget\";\nexport { LabelWidget } from \"./label-widget\";\nexport { DirectionalLinkWidget, LinkWidget } from \"./link-widget\";\nexport { OutputWidget } from \"./output-widget\";\nexport { PasswordWidget } from \"./password-widget\";\nexport { PlayWidget } from \"./play-widget\";\nexport { RadioButtonsWidget } from \"./radio-buttons-widget\";\nexport { SelectMultipleWidget } from \"./select-multiple-widget\";\nexport { SelectWidget } from \"./select-widget\";\nexport { SelectionRangeSliderWidget } from \"./selection-range-slider-widget\";\nexport { SelectionSliderWidget } from \"./selection-slider-widget\";\nexport { StackWidget } from \"./stack-widget\";\nexport { TabWidget } from \"./tab-widget\";\nexport { TagsInputWidget } from \"./tags-input-widget\";\nexport { TextWidget } from \"./text-widget\";\nexport { TextareaWidget } from \"./textarea-widget\";\nexport { TimePickerWidget } from \"./time-picker-widget\";\nexport { ToggleButtonWidget } from \"./toggle-button-widget\";\nexport { ToggleButtonsWidget } from \"./toggle-buttons-widget\";\nexport { ValidWidget } from \"./valid-widget\";\n// Re-export layout widgets\nexport { VBoxWidget } from \"./vbox-widget\";\nexport { VideoWidget } from \"./video-widget\";\n",
      "type": "registry:lib",
      "target": "components/widgets/controls/index.ts"
    },
    {
      "path": "registry/widgets/use-layout-styles.ts",
      "content": "import type { CSSProperties } from \"react\";\nimport { useMemo } from \"react\";\nimport {\n  extractChildGridStyles,\n  extractContainerGridStyles,\n  extractGeneralStyles,\n  hasContainerGridStyles,\n} from \"./layout-utils\";\nimport type { WidgetModel } from \"./widget-store\";\nimport { useResolvedModelValue } from \"./widget-store-context\";\n\n/**\n * Hook to extract CSS styles from a widget's Layout model.\n *\n * ipywidgets widgets have a `layout` property that references a LayoutModel.\n * This hook resolves that reference and extracts CSS properties for styling.\n *\n * @param modelId - The widget model ID\n * @returns Object with containerStyle (for grid containers) and childStyle (for grid_area positioning)\n */\nexport function useLayoutStyles(modelId: string): {\n  /** CSS grid container styles (grid_template_columns, grid_template_rows, etc.) + general styles */\n  containerStyle: CSSProperties;\n  /** CSS child placement styles (grid_area, grid_row, grid_column) */\n  childStyle: CSSProperties;\n  /** Whether the layout has grid container properties */\n  hasGridLayout: boolean;\n} {\n  // Resolve the layout model reference (IPY_MODEL_xxx -> LayoutModel)\n  const layoutModel = useResolvedModelValue(modelId, \"layout\") as\n    | WidgetModel\n    | undefined;\n\n  return useMemo(() => {\n    if (!layoutModel?.state) {\n      return {\n        containerStyle: {},\n        childStyle: {},\n        hasGridLayout: false,\n      };\n    }\n\n    const state = layoutModel.state;\n\n    // Extract different style categories\n    const gridContainerStyles = extractContainerGridStyles(state);\n    const generalStyles = extractGeneralStyles(state);\n    const childStyles = extractChildGridStyles(state);\n\n    // Combine container grid styles with general styles for the container\n    const containerStyle: CSSProperties = {\n      ...generalStyles,\n      ...gridContainerStyles,\n    };\n\n    return {\n      containerStyle,\n      childStyle: childStyles,\n      hasGridLayout: hasContainerGridStyles(state),\n    };\n  }, [layoutModel]);\n}\n",
      "type": "registry:lib",
      "target": "components/widgets/use-layout-styles.ts"
    },
    {
      "path": "registry/widgets/layout-utils.ts",
      "content": "import type { CSSProperties } from \"react\";\n\n/**\n * Utilities for converting ipywidgets Layout model properties to CSS.\n *\n * ipywidgets uses snake_case for CSS properties (e.g., grid_template_columns),\n * which need to be converted to camelCase for React's style prop.\n */\n\n/**\n * Map of ipywidgets Layout model property names to React CSS property names.\n * Only includes properties that need explicit mapping (non-trivial conversions).\n */\nconst LAYOUT_CSS_MAP: Record<string, string> = {\n  // Grid container properties\n  grid_template_columns: \"gridTemplateColumns\",\n  grid_template_rows: \"gridTemplateRows\",\n  grid_template_areas: \"gridTemplateAreas\",\n  grid_auto_columns: \"gridAutoColumns\",\n  grid_auto_rows: \"gridAutoRows\",\n  grid_auto_flow: \"gridAutoFlow\",\n  grid_gap: \"gridGap\",\n  // Grid child placement properties\n  grid_row: \"gridRow\",\n  grid_column: \"gridColumn\",\n  grid_area: \"gridArea\",\n  // Size properties\n  min_width: \"minWidth\",\n  max_width: \"maxWidth\",\n  min_height: \"minHeight\",\n  max_height: \"maxHeight\",\n  // Flexbox properties\n  flex_flow: \"flexFlow\",\n  align_items: \"alignItems\",\n  align_self: \"alignSelf\",\n  align_content: \"alignContent\",\n  justify_content: \"justifyContent\",\n  justify_items: \"justifyItems\",\n  // Text properties\n  object_fit: \"objectFit\",\n  object_position: \"objectPosition\",\n  // Overflow\n  overflow_x: \"overflowX\",\n  overflow_y: \"overflowY\",\n};\n\n/**\n * Grid container properties that apply to the parent grid element.\n */\nconst CONTAINER_GRID_PROPERTIES = new Set([\n  \"grid_template_columns\",\n  \"grid_template_rows\",\n  \"grid_template_areas\",\n  \"grid_auto_columns\",\n  \"grid_auto_rows\",\n  \"grid_auto_flow\",\n  \"grid_gap\",\n]);\n\n/**\n * Child placement properties that apply to grid children.\n */\nconst CHILD_GRID_PROPERTIES = new Set([\"grid_row\", \"grid_column\", \"grid_area\"]);\n\n/**\n * General layout properties that can apply to any element.\n */\nconst GENERAL_PROPERTIES = new Set([\n  \"width\",\n  \"height\",\n  \"min_width\",\n  \"max_width\",\n  \"min_height\",\n  \"max_height\",\n  \"margin\",\n  \"padding\",\n  \"border\",\n  \"overflow\",\n  \"overflow_x\",\n  \"overflow_y\",\n  \"visibility\",\n  \"display\",\n  \"flex\",\n  \"flex_flow\",\n  \"align_items\",\n  \"align_self\",\n  \"align_content\",\n  \"justify_content\",\n  \"justify_items\",\n  \"order\",\n  \"object_fit\",\n  \"object_position\",\n]);\n\n/**\n * Convert a snake_case property name to camelCase.\n */\nfunction snakeToCamelCase(str: string): string {\n  return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());\n}\n\n/**\n * Convert an ipywidgets Layout property name to a React CSS property name.\n */\nfunction toReactCSSProperty(snakeCase: string): string {\n  return LAYOUT_CSS_MAP[snakeCase] ?? snakeToCamelCase(snakeCase);\n}\n\n/**\n * Extract CSS properties from a Layout model state.\n *\n * @param state - The Layout model's state object\n * @param propertyFilter - Optional set of property names to include\n * @returns React CSSProperties object\n */\nexport function layoutStateToCSS(\n  state: Record<string, unknown>,\n  propertyFilter?: Set<string>,\n): CSSProperties {\n  const style: Record<string, string> = {};\n\n  for (const [key, value] of Object.entries(state)) {\n    // Skip internal ipywidgets properties\n    if (key.startsWith(\"_\")) continue;\n    // Skip null, undefined, or empty string values\n    if (value === null || value === undefined || value === \"\") continue;\n    // Apply filter if provided\n    if (propertyFilter && !propertyFilter.has(key)) continue;\n    // Only include string values (CSS values)\n    if (typeof value !== \"string\") continue;\n\n    const cssProperty = toReactCSSProperty(key);\n    style[cssProperty] = value;\n  }\n\n  return style as CSSProperties;\n}\n\n/**\n * Extract container grid CSS properties from a Layout model state.\n * These properties should be applied to grid container elements.\n */\nexport function extractContainerGridStyles(\n  state: Record<string, unknown>,\n): CSSProperties {\n  return layoutStateToCSS(state, CONTAINER_GRID_PROPERTIES);\n}\n\n/**\n * Extract child placement CSS properties from a Layout model state.\n * These properties should be applied to grid children for positioning.\n */\nexport function extractChildGridStyles(\n  state: Record<string, unknown>,\n): CSSProperties {\n  return layoutStateToCSS(state, CHILD_GRID_PROPERTIES);\n}\n\n/**\n * Extract general layout CSS properties from a Layout model state.\n * These can be applied to any element (width, height, margin, etc.).\n */\nexport function extractGeneralStyles(\n  state: Record<string, unknown>,\n): CSSProperties {\n  return layoutStateToCSS(state, GENERAL_PROPERTIES);\n}\n\n/**\n * Check if a Layout model state has any grid container properties.\n */\nexport function hasContainerGridStyles(\n  state: Record<string, unknown>,\n): boolean {\n  for (const key of CONTAINER_GRID_PROPERTIES) {\n    const value = state[key];\n    if (value !== null && value !== undefined && value !== \"\") {\n      return true;\n    }\n  }\n  return false;\n}\n",
      "type": "registry:lib",
      "target": "components/widgets/layout-utils.ts"
    },
    {
      "path": "registry/widgets/controls/int-slider.tsx",
      "content": "/**\n * IntSlider widget - renders an integer slider.\n *\n * Maps to ipywidgets IntSliderModel.\n */\n\nimport { Label } from \"@/components/ui/label\";\nimport { Slider } from \"@/components/ui/slider\";\nimport { cn } from \"@/lib/utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport {\n  useWidgetModelValue,\n  useWidgetStoreRequired,\n} from \"../widget-store-context\";\n\nexport function IntSlider({ modelId, className }: WidgetComponentProps) {\n  const { sendUpdate } = useWidgetStoreRequired();\n\n  // Subscribe to individual state keys for fine-grained updates\n  const value = useWidgetModelValue<number>(modelId, \"value\") ?? 0;\n  const min = useWidgetModelValue<number>(modelId, \"min\") ?? 0;\n  const max = useWidgetModelValue<number>(modelId, \"max\") ?? 100;\n  const step = useWidgetModelValue<number>(modelId, \"step\") ?? 1;\n  const description = useWidgetModelValue<string>(modelId, \"description\");\n  const disabled = useWidgetModelValue<boolean>(modelId, \"disabled\") ?? false;\n  const orientation =\n    useWidgetModelValue<\"horizontal\" | \"vertical\">(modelId, \"orientation\") ??\n    \"horizontal\";\n  const readout = useWidgetModelValue<boolean>(modelId, \"readout\") ?? true;\n\n  const handleChange = (newValue: number[]) => {\n    // Round to step and clamp to range\n    const v = Math.round(newValue[0] / step) * step;\n    const clamped = Math.min(max, Math.max(min, v));\n    sendUpdate(modelId, { value: clamped });\n  };\n\n  const isVertical = orientation === \"vertical\";\n\n  return (\n    <div\n      className={cn(\n        \"flex gap-3\",\n        isVertical ? \"flex-col items-center\" : \"items-center\",\n        className,\n      )}\n      data-widget-id={modelId}\n      data-widget-type=\"IntSlider\"\n    >\n      {description && <Label className=\"shrink-0 text-sm\">{description}</Label>}\n      <Slider\n        value={[value]}\n        min={min}\n        max={max}\n        step={step}\n        disabled={disabled}\n        orientation={orientation}\n        onValueChange={handleChange}\n        className={isVertical ? \"h-32\" : \"flex-1 min-w-24\"}\n      />\n      {readout && (\n        <span className=\"w-12 text-right tabular-nums text-sm text-muted-foreground\">\n          {value}\n        </span>\n      )}\n    </div>\n  );\n}\n\nexport default IntSlider;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/int-slider.tsx"
    },
    {
      "path": "registry/widgets/controls/float-slider.tsx",
      "content": "/**\n * FloatSlider widget - renders a floating point slider.\n *\n * Maps to ipywidgets FloatSliderModel.\n */\n\nimport { Label } from \"@/components/ui/label\";\nimport { Slider } from \"@/components/ui/slider\";\nimport { cn } from \"@/lib/utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport {\n  useWidgetModelValue,\n  useWidgetStoreRequired,\n} from \"../widget-store-context\";\n\nexport function FloatSlider({ modelId, className }: WidgetComponentProps) {\n  const { sendUpdate } = useWidgetStoreRequired();\n\n  // Subscribe to individual state keys for fine-grained updates\n  const value = useWidgetModelValue<number>(modelId, \"value\") ?? 0;\n  const min = useWidgetModelValue<number>(modelId, \"min\") ?? 0;\n  const max = useWidgetModelValue<number>(modelId, \"max\") ?? 100;\n  const step = useWidgetModelValue<number>(modelId, \"step\") ?? 0.1;\n  const description = useWidgetModelValue<string>(modelId, \"description\");\n  const disabled = useWidgetModelValue<boolean>(modelId, \"disabled\") ?? false;\n  const orientation =\n    useWidgetModelValue<\"horizontal\" | \"vertical\">(modelId, \"orientation\") ??\n    \"horizontal\";\n  const readout = useWidgetModelValue<boolean>(modelId, \"readout\") ?? true;\n  const readoutFormat =\n    useWidgetModelValue<string>(modelId, \"readout_format\") ?? \".2f\";\n\n  const handleChange = (newValue: number[]) => {\n    // Clamp to range (no integer rounding for floats)\n    const clamped = Math.min(max, Math.max(min, newValue[0]));\n    sendUpdate(modelId, { value: clamped });\n  };\n\n  // Format value for display based on readout_format\n  const formatValue = (v: number): string => {\n    // Parse Python-style format spec (e.g., \".2f\")\n    const match = readoutFormat.match(/\\.(\\d+)f/);\n    if (match) {\n      return v.toFixed(parseInt(match[1], 10));\n    }\n    return String(v);\n  };\n\n  const isVertical = orientation === \"vertical\";\n\n  return (\n    <div\n      className={cn(\n        \"flex gap-3\",\n        isVertical ? \"flex-col items-center\" : \"items-center\",\n        className,\n      )}\n      data-widget-id={modelId}\n      data-widget-type=\"FloatSlider\"\n    >\n      {description && <Label className=\"shrink-0 text-sm\">{description}</Label>}\n      <Slider\n        value={[value]}\n        min={min}\n        max={max}\n        step={step}\n        disabled={disabled}\n        orientation={orientation}\n        onValueChange={handleChange}\n        className={isVertical ? \"h-32\" : \"flex-1 min-w-24\"}\n      />\n      {readout && (\n        <span className=\"w-16 text-right tabular-nums text-sm text-muted-foreground\">\n          {formatValue(value)}\n        </span>\n      )}\n    </div>\n  );\n}\n\nexport default FloatSlider;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/float-slider.tsx"
    },
    {
      "path": "registry/widgets/controls/int-range-slider.tsx",
      "content": "/**\n * IntRangeSlider widget - renders a dual-handle range slider for integers.\n *\n * Maps to ipywidgets IntRangeSliderModel.\n */\n\nimport { Label } from \"@/components/ui/label\";\nimport { Slider } from \"@/components/ui/slider\";\nimport { cn } from \"@/lib/utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport {\n  useWidgetModelValue,\n  useWidgetStoreRequired,\n} from \"../widget-store-context\";\n\nexport function IntRangeSlider({ modelId, className }: WidgetComponentProps) {\n  const { sendUpdate } = useWidgetStoreRequired();\n\n  // ipywidgets uses \"value\" as a tuple [lower, upper]\n  const value = useWidgetModelValue<[number, number]>(modelId, \"value\") ?? [\n    25, 75,\n  ];\n  const min = useWidgetModelValue<number>(modelId, \"min\") ?? 0;\n  const max = useWidgetModelValue<number>(modelId, \"max\") ?? 100;\n  const step = useWidgetModelValue<number>(modelId, \"step\") ?? 1;\n  const description = useWidgetModelValue<string>(modelId, \"description\");\n  const disabled = useWidgetModelValue<boolean>(modelId, \"disabled\") ?? false;\n  const orientation =\n    useWidgetModelValue<\"horizontal\" | \"vertical\">(modelId, \"orientation\") ??\n    \"horizontal\";\n  const readout = useWidgetModelValue<boolean>(modelId, \"readout\") ?? true;\n\n  const handleChange = (newValue: number[]) => {\n    // Round to step and clamp to range\n    const lower = Math.round(newValue[0] / step) * step;\n    const upper = Math.round(newValue[1] / step) * step;\n    const clampedLower = Math.min(max, Math.max(min, lower));\n    const clampedUpper = Math.min(max, Math.max(min, upper));\n    sendUpdate(modelId, { value: [clampedLower, clampedUpper] });\n  };\n\n  const isVertical = orientation === \"vertical\";\n\n  return (\n    <div\n      className={cn(\n        \"flex gap-3\",\n        isVertical ? \"flex-col items-center\" : \"items-center\",\n        className,\n      )}\n      data-widget-id={modelId}\n      data-widget-type=\"IntRangeSlider\"\n    >\n      {description && <Label className=\"shrink-0 text-sm\">{description}</Label>}\n      <Slider\n        value={value}\n        min={min}\n        max={max}\n        step={step}\n        disabled={disabled}\n        orientation={orientation}\n        onValueChange={handleChange}\n        className={isVertical ? \"h-32\" : \"flex-1 min-w-24\"}\n      />\n      {readout && (\n        <span className=\"w-20 text-right tabular-nums text-sm text-muted-foreground\">\n          {value[0]} – {value[1]}\n        </span>\n      )}\n    </div>\n  );\n}\n\nexport default IntRangeSlider;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/int-range-slider.tsx"
    },
    {
      "path": "registry/widgets/controls/float-range-slider.tsx",
      "content": "/**\n * FloatRangeSlider widget - renders a dual-handle range slider for floats.\n *\n * Maps to ipywidgets FloatRangeSliderModel.\n */\n\nimport { Label } from \"@/components/ui/label\";\nimport { Slider } from \"@/components/ui/slider\";\nimport { cn } from \"@/lib/utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport {\n  useWidgetModelValue,\n  useWidgetStoreRequired,\n} from \"../widget-store-context\";\n\nexport function FloatRangeSlider({ modelId, className }: WidgetComponentProps) {\n  const { sendUpdate } = useWidgetStoreRequired();\n\n  // ipywidgets uses \"value\" as a tuple [lower, upper]\n  const value = useWidgetModelValue<[number, number]>(modelId, \"value\") ?? [\n    25.0, 75.0,\n  ];\n  const min = useWidgetModelValue<number>(modelId, \"min\") ?? 0;\n  const max = useWidgetModelValue<number>(modelId, \"max\") ?? 100;\n  const step = useWidgetModelValue<number>(modelId, \"step\") ?? 0.1;\n  const description = useWidgetModelValue<string>(modelId, \"description\");\n  const disabled = useWidgetModelValue<boolean>(modelId, \"disabled\") ?? false;\n  const orientation =\n    useWidgetModelValue<\"horizontal\" | \"vertical\">(modelId, \"orientation\") ??\n    \"horizontal\";\n  const readout = useWidgetModelValue<boolean>(modelId, \"readout\") ?? true;\n  const readoutFormat =\n    useWidgetModelValue<string>(modelId, \"readout_format\") ?? \".2f\";\n\n  const handleChange = (newValue: number[]) => {\n    // Clamp to range (no integer rounding for floats)\n    const clampedLower = Math.min(max, Math.max(min, newValue[0]));\n    const clampedUpper = Math.min(max, Math.max(min, newValue[1]));\n    sendUpdate(modelId, { value: [clampedLower, clampedUpper] });\n  };\n\n  // Format value for display based on readout_format\n  const formatValue = (v: number): string => {\n    // Parse Python-style format spec (e.g., \".2f\")\n    const match = readoutFormat.match(/\\.(\\d+)f/);\n    if (match) {\n      return v.toFixed(parseInt(match[1], 10));\n    }\n    return String(v);\n  };\n\n  const isVertical = orientation === \"vertical\";\n\n  return (\n    <div\n      className={cn(\n        \"flex gap-3\",\n        isVertical ? \"flex-col items-center\" : \"items-center\",\n        className,\n      )}\n      data-widget-id={modelId}\n      data-widget-type=\"FloatRangeSlider\"\n    >\n      {description && <Label className=\"shrink-0 text-sm\">{description}</Label>}\n      <Slider\n        value={value}\n        min={min}\n        max={max}\n        step={step}\n        disabled={disabled}\n        orientation={orientation}\n        onValueChange={handleChange}\n        className={isVertical ? \"h-32\" : \"flex-1 min-w-24\"}\n      />\n      {readout && (\n        <span className=\"w-28 text-right tabular-nums text-sm text-muted-foreground\">\n          {formatValue(value[0])} – {formatValue(value[1])}\n        </span>\n      )}\n    </div>\n  );\n}\n\nexport default FloatRangeSlider;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/float-range-slider.tsx"
    },
    {
      "path": "registry/widgets/controls/int-progress.tsx",
      "content": "/**\n * IntProgress widget - renders an integer progress bar.\n *\n * Maps to ipywidgets IntProgressModel.\n */\n\nimport { Label } from \"@/components/ui/label\";\nimport { Progress } from \"@/components/ui/progress\";\nimport { cn } from \"@/lib/utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport { useWidgetModelValue } from \"../widget-store-context\";\n\nexport function IntProgress({ modelId, className }: WidgetComponentProps) {\n  // Subscribe to individual state keys\n  const value = useWidgetModelValue<number>(modelId, \"value\") ?? 0;\n  const min = useWidgetModelValue<number>(modelId, \"min\") ?? 0;\n  const max = useWidgetModelValue<number>(modelId, \"max\") ?? 100;\n  const description = useWidgetModelValue<string>(modelId, \"description\");\n  const barStyle =\n    useWidgetModelValue<\"success\" | \"info\" | \"warning\" | \"danger\" | \"\">(\n      modelId,\n      \"bar_style\",\n    ) ?? \"\";\n  const orientation =\n    useWidgetModelValue<\"horizontal\" | \"vertical\">(modelId, \"orientation\") ??\n    \"horizontal\";\n\n  // Calculate percentage\n  const range = max - min;\n  const percentage = range > 0 ? ((value - min) / range) * 100 : 0;\n\n  // Map bar_style to Tailwind classes\n  const barStyleClasses: Record<string, string> = {\n    success: \"[&>[data-slot=progress-indicator]]:bg-green-500\",\n    info: \"[&>[data-slot=progress-indicator]]:bg-blue-500\",\n    warning: \"[&>[data-slot=progress-indicator]]:bg-yellow-500\",\n    danger: \"[&>[data-slot=progress-indicator]]:bg-red-500\",\n  };\n\n  const isVertical = orientation === \"vertical\";\n\n  return (\n    <div\n      className={cn(\n        \"flex gap-3\",\n        isVertical ? \"flex-col items-center\" : \"flex-1 items-center\",\n        className,\n      )}\n      data-widget-id={modelId}\n      data-widget-type=\"IntProgress\"\n    >\n      {description && <Label className=\"shrink-0 text-sm\">{description}</Label>}\n      <Progress\n        value={percentage}\n        className={cn(\n          isVertical ? \"h-32 w-2\" : \"flex-1 min-w-24\",\n          barStyle && barStyleClasses[barStyle],\n        )}\n      />\n    </div>\n  );\n}\n\nexport default IntProgress;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/int-progress.tsx"
    },
    {
      "path": "registry/widgets/controls/float-progress.tsx",
      "content": "/**\n * FloatProgress widget - renders a floating point progress bar.\n *\n * Maps to ipywidgets FloatProgressModel.\n */\n\nimport { Label } from \"@/components/ui/label\";\nimport { Progress } from \"@/components/ui/progress\";\nimport { cn } from \"@/lib/utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport { useWidgetModelValue } from \"../widget-store-context\";\n\nexport function FloatProgress({ modelId, className }: WidgetComponentProps) {\n  // Subscribe to individual state keys\n  const value = useWidgetModelValue<number>(modelId, \"value\") ?? 0;\n  const min = useWidgetModelValue<number>(modelId, \"min\") ?? 0;\n  const max = useWidgetModelValue<number>(modelId, \"max\") ?? 100;\n  const description = useWidgetModelValue<string>(modelId, \"description\");\n  const barStyle =\n    useWidgetModelValue<\"success\" | \"info\" | \"warning\" | \"danger\" | \"\">(\n      modelId,\n      \"bar_style\",\n    ) ?? \"\";\n  const orientation =\n    useWidgetModelValue<\"horizontal\" | \"vertical\">(modelId, \"orientation\") ??\n    \"horizontal\";\n\n  // Calculate percentage\n  const range = max - min;\n  const percentage = range > 0 ? ((value - min) / range) * 100 : 0;\n\n  // Map bar_style to Tailwind classes\n  const barStyleClasses: Record<string, string> = {\n    success: \"[&>[data-slot=progress-indicator]]:bg-green-500\",\n    info: \"[&>[data-slot=progress-indicator]]:bg-blue-500\",\n    warning: \"[&>[data-slot=progress-indicator]]:bg-yellow-500\",\n    danger: \"[&>[data-slot=progress-indicator]]:bg-red-500\",\n  };\n\n  const isVertical = orientation === \"vertical\";\n\n  return (\n    <div\n      className={cn(\n        \"flex gap-3\",\n        isVertical ? \"flex-col items-center\" : \"flex-1 items-center\",\n        className,\n      )}\n      data-widget-id={modelId}\n      data-widget-type=\"FloatProgress\"\n    >\n      {description && <Label className=\"shrink-0 text-sm\">{description}</Label>}\n      <Progress\n        value={percentage}\n        className={cn(\n          isVertical ? \"h-32 w-2\" : \"flex-1 min-w-24\",\n          barStyle && barStyleClasses[barStyle],\n        )}\n      />\n    </div>\n  );\n}\n\nexport default FloatProgress;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/float-progress.tsx"
    },
    {
      "path": "registry/widgets/controls/button-widget.tsx",
      "content": "/**\n * Button widget - renders a clickable button.\n *\n * Maps to ipywidgets ButtonModel.\n */\n\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport {\n  useWidgetModelValue,\n  useWidgetStoreRequired,\n} from \"../widget-store-context\";\nimport { getButtonStyle } from \"./button-style-utils\";\n\nexport function ButtonWidget({ modelId, className }: WidgetComponentProps) {\n  const { sendCustom } = useWidgetStoreRequired();\n\n  // Subscribe to individual state keys\n  const description = useWidgetModelValue<string>(modelId, \"description\") ?? \"\";\n  const disabled = useWidgetModelValue<boolean>(modelId, \"disabled\") ?? false;\n  const buttonStyle =\n    useWidgetModelValue<string>(modelId, \"button_style\") ?? \"\";\n  const icon = useWidgetModelValue<string>(modelId, \"icon\");\n  const tooltip = useWidgetModelValue<string>(modelId, \"tooltip\");\n\n  const handleClick = () => {\n    // Send click event to kernel\n    sendCustom(modelId, { event: \"click\" });\n  };\n\n  const { variant, className: styleClassName } = getButtonStyle(buttonStyle);\n\n  return (\n    <Button\n      variant={variant}\n      disabled={disabled}\n      onClick={handleClick}\n      title={tooltip}\n      className={cn(styleClassName, className)}\n      data-widget-id={modelId}\n      data-widget-type=\"Button\"\n    >\n      {icon && <span className=\"mr-1\">{icon}</span>}\n      {description || \"Button\"}\n    </Button>\n  );\n}\n\nexport default ButtonWidget;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/button-widget.tsx"
    },
    {
      "path": "registry/widgets/controls/checkbox-widget.tsx",
      "content": "/**\n * Checkbox widget - renders a checkbox input.\n *\n * Maps to ipywidgets CheckboxModel.\n */\n\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport { Label } from \"@/components/ui/label\";\nimport { cn } from \"@/lib/utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport {\n  useWidgetModelValue,\n  useWidgetStoreRequired,\n} from \"../widget-store-context\";\n\nexport function CheckboxWidget({ modelId, className }: WidgetComponentProps) {\n  const { sendUpdate } = useWidgetStoreRequired();\n\n  // Subscribe to individual state keys\n  const value = useWidgetModelValue<boolean>(modelId, \"value\") ?? false;\n  const description = useWidgetModelValue<string>(modelId, \"description\");\n  const disabled = useWidgetModelValue<boolean>(modelId, \"disabled\") ?? false;\n  const indent = useWidgetModelValue<boolean>(modelId, \"indent\") ?? true;\n\n  const handleChange = (checked: boolean | \"indeterminate\") => {\n    if (checked !== \"indeterminate\") {\n      sendUpdate(modelId, { value: checked });\n    }\n  };\n\n  return (\n    <div\n      className={cn(\"flex items-center gap-2\", indent && \"pl-4\", className)}\n      data-widget-id={modelId}\n      data-widget-type=\"Checkbox\"\n    >\n      <Checkbox\n        id={`checkbox-${modelId}`}\n        checked={value}\n        disabled={disabled}\n        onCheckedChange={handleChange}\n      />\n      {description && (\n        <Label\n          htmlFor={`checkbox-${modelId}`}\n          className={cn(\"text-sm\", disabled && \"opacity-50 cursor-not-allowed\")}\n        >\n          {description}\n        </Label>\n      )}\n    </div>\n  );\n}\n\nexport default CheckboxWidget;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/checkbox-widget.tsx"
    },
    {
      "path": "registry/widgets/controls/text-widget.tsx",
      "content": "\"use client\";\n\n/**\n * Text widget - renders a text input field.\n *\n * Maps to ipywidgets TextModel.\n */\n\nimport { useCallback, useEffect, useState } from \"react\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { cn } from \"@/lib/utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport {\n  useWidgetModelValue,\n  useWidgetStoreRequired,\n} from \"../widget-store-context\";\n\nexport function TextWidget({ modelId, className }: WidgetComponentProps) {\n  // sendUpdate and sendCustom are now stable (useCommRouter uses refs internally)\n  const { sendUpdate, sendCustom } = useWidgetStoreRequired();\n\n  // Subscribe to individual state keys\n  const value = useWidgetModelValue<string>(modelId, \"value\") ?? \"\";\n  const description = useWidgetModelValue<string>(modelId, \"description\");\n  const placeholder = useWidgetModelValue<string>(modelId, \"placeholder\") ?? \"\";\n  const disabled = useWidgetModelValue<boolean>(modelId, \"disabled\") ?? false;\n  const continuousUpdate =\n    useWidgetModelValue<boolean>(modelId, \"continuous_update\") ?? true;\n\n  // Local state for non-continuous updates\n  const [localValue, setLocalValue] = useState(value);\n\n  // Sync local state when value changes from kernel\n  useEffect(() => {\n    setLocalValue(value);\n  }, [value]);\n\n  const handleChange = useCallback(\n    (e: React.ChangeEvent<HTMLInputElement>) => {\n      const newValue = e.target.value;\n      setLocalValue(newValue);\n\n      if (continuousUpdate) {\n        sendUpdate(modelId, { value: newValue });\n      }\n    },\n    [modelId, continuousUpdate, sendUpdate],\n  );\n\n  const handleBlur = useCallback(() => {\n    if (!continuousUpdate && localValue !== value) {\n      sendUpdate(modelId, { value: localValue });\n    }\n  }, [modelId, continuousUpdate, localValue, value, sendUpdate]);\n\n  const handleKeyDown = useCallback(\n    (e: React.KeyboardEvent<HTMLInputElement>) => {\n      if (e.key === \"Enter\") {\n        // Send submit event\n        sendCustom(modelId, { event: \"submit\" });\n        // Also ensure value is synced\n        if (!continuousUpdate && localValue !== value) {\n          sendUpdate(modelId, { value: localValue });\n        }\n      }\n    },\n    [modelId, continuousUpdate, localValue, value, sendUpdate, sendCustom],\n  );\n\n  return (\n    <div\n      className={cn(\"flex items-center gap-3\", className)}\n      data-widget-id={modelId}\n      data-widget-type=\"Text\"\n    >\n      {description && <Label className=\"shrink-0 text-sm\">{description}</Label>}\n      <Input\n        type=\"text\"\n        value={localValue}\n        placeholder={placeholder}\n        disabled={disabled}\n        onChange={handleChange}\n        onBlur={handleBlur}\n        onKeyDown={handleKeyDown}\n        className=\"flex-1\"\n      />\n    </div>\n  );\n}\n\nexport default TextWidget;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/text-widget.tsx"
    },
    {
      "path": "registry/widgets/controls/textarea-widget.tsx",
      "content": "\"use client\";\n\n/**\n * Textarea widget - renders a multi-line text input.\n *\n * Maps to ipywidgets TextareaModel.\n */\n\nimport { useCallback, useEffect, useState } from \"react\";\nimport { Label } from \"@/components/ui/label\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { cn } from \"@/lib/utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport {\n  useWidgetModelValue,\n  useWidgetStoreRequired,\n} from \"../widget-store-context\";\n\nexport function TextareaWidget({ modelId, className }: WidgetComponentProps) {\n  const { sendUpdate } = useWidgetStoreRequired();\n\n  // Subscribe to individual state keys\n  const value = useWidgetModelValue<string>(modelId, \"value\") ?? \"\";\n  const description = useWidgetModelValue<string>(modelId, \"description\");\n  const placeholder = useWidgetModelValue<string>(modelId, \"placeholder\") ?? \"\";\n  const disabled = useWidgetModelValue<boolean>(modelId, \"disabled\") ?? false;\n  const rows = useWidgetModelValue<number>(modelId, \"rows\") ?? 4;\n  const continuousUpdate =\n    useWidgetModelValue<boolean>(modelId, \"continuous_update\") ?? true;\n\n  // Local state for non-continuous updates\n  const [localValue, setLocalValue] = useState(value);\n\n  // Sync local state when value changes from kernel\n  useEffect(() => {\n    setLocalValue(value);\n  }, [value]);\n\n  const handleChange = useCallback(\n    (e: React.ChangeEvent<HTMLTextAreaElement>) => {\n      const newValue = e.target.value;\n      setLocalValue(newValue);\n\n      if (continuousUpdate) {\n        sendUpdate(modelId, { value: newValue });\n      }\n    },\n    [modelId, continuousUpdate, sendUpdate],\n  );\n\n  const handleBlur = useCallback(() => {\n    if (!continuousUpdate && localValue !== value) {\n      sendUpdate(modelId, { value: localValue });\n    }\n  }, [modelId, continuousUpdate, localValue, value, sendUpdate]);\n\n  return (\n    <div\n      className={cn(\"flex flex-col gap-2\", className)}\n      data-widget-id={modelId}\n      data-widget-type=\"Textarea\"\n    >\n      {description && <Label className=\"text-sm\">{description}</Label>}\n      <Textarea\n        value={localValue}\n        placeholder={placeholder}\n        disabled={disabled}\n        rows={rows}\n        onChange={handleChange}\n        onBlur={handleBlur}\n        className=\"resize-y\"\n      />\n    </div>\n  );\n}\n\nexport default TextareaWidget;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/textarea-widget.tsx"
    },
    {
      "path": "registry/widgets/controls/html-widget.tsx",
      "content": "/**\n * HTML widget - renders arbitrary HTML content.\n *\n * Maps to ipywidgets HTMLModel.\n */\n\nimport { Label } from \"@/components/ui/label\";\nimport { cn } from \"@/lib/utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport { useWidgetModelValue } from \"../widget-store-context\";\n\nexport function HTMLWidget({ modelId, className }: WidgetComponentProps) {\n  const value = useWidgetModelValue<string>(modelId, \"value\") ?? \"\";\n  const description = useWidgetModelValue<string>(modelId, \"description\");\n  const placeholder = useWidgetModelValue<string>(modelId, \"placeholder\");\n\n  // Show placeholder if value is empty\n  const displayValue = value || placeholder || \"\";\n\n  return (\n    <div\n      className={cn(\"inline-flex shrink-0 items-baseline gap-1\", className)}\n      data-widget-id={modelId}\n      data-widget-type=\"HTML\"\n    >\n      {description && <Label className=\"shrink-0 text-sm\">{description}</Label>}\n      <div\n        className=\"widget-html-content\"\n        dangerouslySetInnerHTML={{ __html: displayValue }}\n      />\n    </div>\n  );\n}\n\nexport default HTMLWidget;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/html-widget.tsx"
    },
    {
      "path": "registry/widgets/controls/color-picker.tsx",
      "content": "/**\n * ColorPicker widget - renders a color selection input.\n *\n * Maps to ipywidgets ColorPickerModel.\n */\n\nimport { Label } from \"@/components/ui/label\";\nimport { cn } from \"@/lib/utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport {\n  useWidgetModelValue,\n  useWidgetStoreRequired,\n} from \"../widget-store-context\";\n\nexport function ColorPicker({ modelId, className }: WidgetComponentProps) {\n  const { sendUpdate } = useWidgetStoreRequired();\n\n  const value = useWidgetModelValue<string>(modelId, \"value\") ?? \"#000000\";\n  const description = useWidgetModelValue<string>(modelId, \"description\");\n  const disabled = useWidgetModelValue<boolean>(modelId, \"disabled\") ?? false;\n  const concise = useWidgetModelValue<boolean>(modelId, \"concise\") ?? false;\n\n  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    sendUpdate(modelId, { value: e.target.value });\n  };\n\n  return (\n    <div\n      className={cn(\"flex items-center gap-3\", className)}\n      data-widget-id={modelId}\n      data-widget-type=\"ColorPicker\"\n    >\n      {description && <Label className=\"shrink-0 text-sm\">{description}</Label>}\n      <input\n        type=\"color\"\n        value={value}\n        disabled={disabled}\n        onChange={handleChange}\n        className={cn(\n          \"h-8 w-12 cursor-pointer rounded border border-input bg-transparent p-0.5\",\n          \"disabled:cursor-not-allowed disabled:opacity-50\",\n        )}\n      />\n      {!concise && (\n        <span className=\"text-sm text-muted-foreground font-mono\">{value}</span>\n      )}\n    </div>\n  );\n}\n\nexport default ColorPicker;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/color-picker.tsx"
    },
    {
      "path": "registry/widgets/controls/dropdown-widget.tsx",
      "content": "/**\n * Dropdown widget - renders a native select dropdown.\n *\n * Maps to ipywidgets DropdownModel.\n *\n * Uses native HTML <select> instead of Radix UI Select because Radix\n * uses Portal which doesn't work in sandboxed iframes (no allow-same-origin).\n * See: https://github.com/runtimed/runt/issues/62\n */\n\nimport { Label } from \"@/components/ui/label\";\nimport { cn } from \"@/lib/utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport {\n  useWidgetModelValue,\n  useWidgetStoreRequired,\n} from \"../widget-store-context\";\n\nexport function DropdownWidget({ modelId, className }: WidgetComponentProps) {\n  const { sendUpdate } = useWidgetStoreRequired();\n\n  // Subscribe to individual state keys\n  const options =\n    useWidgetModelValue<string[]>(modelId, \"_options_labels\") ?? [];\n  const index = useWidgetModelValue<number | null>(modelId, \"index\");\n  const description = useWidgetModelValue<string>(modelId, \"description\");\n  const disabled = useWidgetModelValue<boolean>(modelId, \"disabled\") ?? false;\n\n  // Convert index to string value for select\n  const value =\n    index !== null && index !== undefined && index >= 0 ? String(index) : \"\";\n\n  const handleChange = (event: React.ChangeEvent<HTMLSelectElement>) => {\n    const newIndex = parseInt(event.target.value, 10);\n    if (!Number.isNaN(newIndex)) {\n      sendUpdate(modelId, { index: newIndex });\n    }\n  };\n\n  return (\n    <div\n      className={cn(\"flex items-center gap-3\", className)}\n      data-widget-id={modelId}\n      data-widget-type=\"Dropdown\"\n    >\n      {description && <Label className=\"shrink-0 text-sm\">{description}</Label>}\n      <select\n        value={value}\n        onChange={handleChange}\n        disabled={disabled}\n        className={cn(\n          // Base styles matching shadcn SelectTrigger\n          \"h-9 w-48 appearance-none rounded-md border border-input px-3 py-2 text-sm\",\n          \"bg-transparent shadow-xs transition-[color,box-shadow] outline-none\",\n          // Dark mode background\n          \"dark:bg-input/30 dark:hover:bg-input/50\",\n          // Focus styles\n          \"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50\",\n          // Disabled state\n          \"disabled:cursor-not-allowed disabled:opacity-50\",\n          // Custom dropdown arrow using background image (chevron-down)\n          \"bg-[length:16px_16px] bg-[right_8px_center] bg-no-repeat\",\n          \"bg-[url('data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22%239ca3af%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22m6%209%206%206%206-6%22%2F%3E%3C%2Fsvg%3E')]\",\n          \"pr-9\", // Extra padding for the arrow\n        )}\n      >\n        {options.map((option, idx) => (\n          <option key={idx} value={String(idx)}>\n            {option}\n          </option>\n        ))}\n      </select>\n    </div>\n  );\n}\n\nexport default DropdownWidget;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/dropdown-widget.tsx"
    },
    {
      "path": "registry/widgets/controls/radio-buttons-widget.tsx",
      "content": "/**\n * RadioButtons widget - renders a group of radio buttons.\n *\n * Maps to ipywidgets RadioButtonsModel.\n */\n\nimport { Label } from \"@/components/ui/label\";\nimport { RadioGroup, RadioGroupItem } from \"@/components/ui/radio-group\";\nimport { cn } from \"@/lib/utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport {\n  useWidgetModelValue,\n  useWidgetStoreRequired,\n} from \"../widget-store-context\";\n\nexport function RadioButtonsWidget({\n  modelId,\n  className,\n}: WidgetComponentProps) {\n  const { sendUpdate } = useWidgetStoreRequired();\n\n  // Subscribe to individual state keys\n  const options =\n    useWidgetModelValue<string[]>(modelId, \"_options_labels\") ?? [];\n  const index = useWidgetModelValue<number | null>(modelId, \"index\");\n  const description = useWidgetModelValue<string>(modelId, \"description\");\n  const disabled = useWidgetModelValue<boolean>(modelId, \"disabled\") ?? false;\n\n  // Convert index to string value for RadioGroup\n  const value =\n    index !== null && index !== undefined && index >= 0\n      ? String(index)\n      : undefined;\n\n  const handleValueChange = (newValue: string) => {\n    const newIndex = parseInt(newValue, 10);\n    if (!Number.isNaN(newIndex)) {\n      sendUpdate(modelId, { index: newIndex });\n    }\n  };\n\n  return (\n    <div\n      className={cn(\"flex items-start gap-3\", className)}\n      data-widget-id={modelId}\n      data-widget-type=\"RadioButtons\"\n    >\n      {description && (\n        <Label className=\"shrink-0 text-sm pt-0.5\">{description}</Label>\n      )}\n      <RadioGroup\n        value={value}\n        onValueChange={handleValueChange}\n        disabled={disabled}\n      >\n        {options.map((option, idx) => (\n          <div key={idx} className=\"flex items-center gap-2\">\n            <RadioGroupItem\n              value={String(idx)}\n              id={`${modelId}-radio-${idx}`}\n              disabled={disabled}\n            />\n            <Label\n              htmlFor={`${modelId}-radio-${idx}`}\n              className={cn(\n                \"text-sm font-normal cursor-pointer\",\n                disabled && \"opacity-50 cursor-not-allowed\",\n              )}\n            >\n              {option}\n            </Label>\n          </div>\n        ))}\n      </RadioGroup>\n    </div>\n  );\n}\n\nexport default RadioButtonsWidget;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/radio-buttons-widget.tsx"
    },
    {
      "path": "registry/widgets/controls/select-multiple-widget.tsx",
      "content": "/**\n * SelectMultiple widget - renders a multi-select listbox.\n *\n * Maps to ipywidgets SelectMultipleModel.\n */\n\nimport { CheckIcon } from \"lucide-react\";\nimport { Label } from \"@/components/ui/label\";\nimport { cn } from \"@/lib/utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport {\n  useWidgetModelValue,\n  useWidgetStoreRequired,\n} from \"../widget-store-context\";\n\nexport function SelectMultipleWidget({\n  modelId,\n  className,\n}: WidgetComponentProps) {\n  const { sendUpdate } = useWidgetStoreRequired();\n\n  // Subscribe to individual state keys\n  const options =\n    useWidgetModelValue<string[]>(modelId, \"_options_labels\") ?? [];\n  const selectedIndices = useWidgetModelValue<number[]>(modelId, \"index\") ?? [];\n  const description = useWidgetModelValue<string>(modelId, \"description\");\n  const disabled = useWidgetModelValue<boolean>(modelId, \"disabled\") ?? false;\n  const rows = useWidgetModelValue<number>(modelId, \"rows\") ?? 5;\n\n  const handleToggle = (idx: number) => {\n    if (disabled) return;\n\n    const newIndices = selectedIndices.includes(idx)\n      ? selectedIndices.filter((i) => i !== idx)\n      : [...selectedIndices, idx].sort((a, b) => a - b);\n\n    sendUpdate(modelId, { index: newIndices });\n  };\n\n  // Calculate height based on rows\n  const itemHeight = 32; // approximate height per item in px\n  const maxHeight = rows * itemHeight;\n\n  return (\n    <div\n      className={cn(\"flex items-start gap-3\", className)}\n      data-widget-id={modelId}\n      data-widget-type=\"SelectMultiple\"\n    >\n      {description && (\n        <Label className=\"shrink-0 text-sm pt-1\">{description}</Label>\n      )}\n      <div\n        role=\"listbox\"\n        aria-multiselectable=\"true\"\n        aria-disabled={disabled}\n        className={cn(\n          \"w-48 overflow-y-auto rounded-md border border-input bg-background shadow-xs\",\n          disabled && \"opacity-50 cursor-not-allowed\",\n        )}\n        style={{ maxHeight }}\n      >\n        {options.map((option, idx) => {\n          const isSelected = selectedIndices.includes(idx);\n          return (\n            <div\n              key={idx}\n              role=\"option\"\n              aria-selected={isSelected}\n              onClick={() => handleToggle(idx)}\n              className={cn(\n                \"flex items-center gap-2 px-3 py-1.5 text-sm cursor-pointer select-none\",\n                \"hover:bg-accent hover:text-accent-foreground\",\n                isSelected && \"bg-accent/50\",\n                disabled && \"pointer-events-none\",\n              )}\n            >\n              <span className=\"flex size-4 items-center justify-center\">\n                {isSelected && <CheckIcon className=\"size-3\" />}\n              </span>\n              <span>{option}</span>\n            </div>\n          );\n        })}\n      </div>\n    </div>\n  );\n}\n\nexport default SelectMultipleWidget;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/select-multiple-widget.tsx"
    },
    {
      "path": "registry/widgets/controls/toggle-button-widget.tsx",
      "content": "/**\n * ToggleButton widget - renders a single toggle button.\n *\n * Maps to ipywidgets ToggleButtonModel.\n */\n\nimport { Toggle } from \"@/components/ui/toggle\";\nimport { cn } from \"@/lib/utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport {\n  useWidgetModelValue,\n  useWidgetStoreRequired,\n} from \"../widget-store-context\";\n\nexport function ToggleButtonWidget({\n  modelId,\n  className,\n}: WidgetComponentProps) {\n  const { sendUpdate } = useWidgetStoreRequired();\n\n  // Subscribe to individual state keys\n  const value = useWidgetModelValue<boolean>(modelId, \"value\") ?? false;\n  const description = useWidgetModelValue<string>(modelId, \"description\") ?? \"\";\n  const disabled = useWidgetModelValue<boolean>(modelId, \"disabled\") ?? false;\n  const icon = useWidgetModelValue<string>(modelId, \"icon\");\n  const buttonStyle =\n    useWidgetModelValue<string>(modelId, \"button_style\") ?? \"\";\n\n  const handlePressedChange = (pressed: boolean) => {\n    sendUpdate(modelId, { value: pressed });\n  };\n\n  // Map button_style to variant - outline for styled buttons\n  const variant = buttonStyle ? \"outline\" : \"default\";\n\n  return (\n    <Toggle\n      pressed={value}\n      onPressedChange={handlePressedChange}\n      disabled={disabled}\n      variant={variant}\n      className={cn(className)}\n      data-widget-id={modelId}\n      data-widget-type=\"ToggleButton\"\n    >\n      {icon && <span className=\"mr-1\">{icon}</span>}\n      {description || \"Toggle\"}\n    </Toggle>\n  );\n}\n\nexport default ToggleButtonWidget;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/toggle-button-widget.tsx"
    },
    {
      "path": "registry/widgets/controls/toggle-buttons-widget.tsx",
      "content": "/**\n * ToggleButtons widget - renders a group of toggle buttons (single selection).\n *\n * Maps to ipywidgets ToggleButtonsModel.\n */\n\nimport { Label } from \"@/components/ui/label\";\nimport { ToggleGroup, ToggleGroupItem } from \"@/components/ui/toggle-group\";\nimport { cn } from \"@/lib/utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport {\n  useWidgetModelValue,\n  useWidgetStoreRequired,\n} from \"../widget-store-context\";\n\nexport function ToggleButtonsWidget({\n  modelId,\n  className,\n}: WidgetComponentProps) {\n  const { sendUpdate } = useWidgetStoreRequired();\n\n  // Subscribe to individual state keys\n  const options =\n    useWidgetModelValue<string[]>(modelId, \"_options_labels\") ?? [];\n  const index = useWidgetModelValue<number | null>(modelId, \"index\");\n  const description = useWidgetModelValue<string>(modelId, \"description\");\n  const disabled = useWidgetModelValue<boolean>(modelId, \"disabled\") ?? false;\n  const icons = useWidgetModelValue<string[]>(modelId, \"icons\") ?? [];\n  const tooltips = useWidgetModelValue<string[]>(modelId, \"tooltips\") ?? [];\n\n  // Convert index to string value for ToggleGroup\n  const value =\n    index !== null && index !== undefined && index >= 0\n      ? String(index)\n      : undefined;\n\n  const handleValueChange = (newValue: string) => {\n    if (newValue === \"\") {\n      // Deselection - ToggleButtons typically require a selection\n      return;\n    }\n    const newIndex = parseInt(newValue, 10);\n    if (!Number.isNaN(newIndex)) {\n      sendUpdate(modelId, { index: newIndex });\n    }\n  };\n\n  return (\n    <div\n      className={cn(\"flex items-center gap-3\", className)}\n      data-widget-id={modelId}\n      data-widget-type=\"ToggleButtons\"\n    >\n      {description && <Label className=\"shrink-0 text-sm\">{description}</Label>}\n      <ToggleGroup\n        type=\"single\"\n        value={value}\n        onValueChange={handleValueChange}\n        disabled={disabled}\n        variant=\"outline\"\n      >\n        {options.map((option, idx) => (\n          <ToggleGroupItem\n            key={idx}\n            value={String(idx)}\n            title={tooltips[idx] || undefined}\n            disabled={disabled}\n          >\n            {icons[idx] && <span className=\"mr-1\">{icons[idx]}</span>}\n            {option}\n          </ToggleGroupItem>\n        ))}\n      </ToggleGroup>\n    </div>\n  );\n}\n\nexport default ToggleButtonsWidget;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/toggle-buttons-widget.tsx"
    },
    {
      "path": "registry/widgets/controls/vbox-widget.tsx",
      "content": "/**\n * VBox widget - vertical flex container.\n *\n * Maps to ipywidgets VBoxModel. Arranges children in a vertical column.\n */\n\nimport { cn } from \"@/lib/utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport { parseModelRef, useWidgetModelValue } from \"../widget-store-context\";\nimport { WidgetView } from \"../widget-view\";\n\n// Map ipywidgets box_style to Tailwind classes\nconst BOX_STYLE_MAP: Record<string, string> = {\n  \"\": \"\",\n  primary:\n    \"border border-blue-500 bg-blue-50/50 dark:bg-blue-950/50 rounded-md p-2\",\n  success:\n    \"border border-green-500 bg-green-50/50 dark:bg-green-950/50 rounded-md p-2\",\n  info: \"border border-sky-500 bg-sky-50/50 dark:bg-sky-950/50 rounded-md p-2\",\n  warning:\n    \"border border-yellow-500 bg-yellow-50/50 dark:bg-yellow-950/50 rounded-md p-2\",\n  danger:\n    \"border border-red-500 bg-red-50/50 dark:bg-red-950/50 rounded-md p-2\",\n};\n\nexport function VBoxWidget({ modelId, className }: WidgetComponentProps) {\n  // Subscribe to individual state keys\n  const children = useWidgetModelValue<string[]>(modelId, \"children\");\n  const boxStyle = useWidgetModelValue<string>(modelId, \"box_style\") ?? \"\";\n\n  const styleClass = BOX_STYLE_MAP[boxStyle] ?? \"\";\n\n  return (\n    <div\n      className={cn(\"flex flex-col gap-2\", styleClass, className)}\n      data-widget-id={modelId}\n      data-widget-type=\"VBox\"\n    >\n      {children?.map((childRef) => {\n        const childId = parseModelRef(childRef);\n        return childId ? <WidgetView key={childId} modelId={childId} /> : null;\n      })}\n    </div>\n  );\n}\n\nexport default VBoxWidget;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/vbox-widget.tsx"
    },
    {
      "path": "registry/widgets/controls/hbox-widget.tsx",
      "content": "/**\n * HBox widget - horizontal flex container.\n *\n * Maps to ipywidgets HBoxModel. Arranges children in a horizontal row.\n */\n\nimport { cn } from \"@/lib/utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport { parseModelRef, useWidgetModelValue } from \"../widget-store-context\";\nimport { WidgetView } from \"../widget-view\";\n\n// Map ipywidgets box_style to Tailwind classes\nconst BOX_STYLE_MAP: Record<string, string> = {\n  \"\": \"\",\n  primary:\n    \"border border-blue-500 bg-blue-50/50 dark:bg-blue-950/50 rounded-md p-2\",\n  success:\n    \"border border-green-500 bg-green-50/50 dark:bg-green-950/50 rounded-md p-2\",\n  info: \"border border-sky-500 bg-sky-50/50 dark:bg-sky-950/50 rounded-md p-2\",\n  warning:\n    \"border border-yellow-500 bg-yellow-50/50 dark:bg-yellow-950/50 rounded-md p-2\",\n  danger:\n    \"border border-red-500 bg-red-50/50 dark:bg-red-950/50 rounded-md p-2\",\n};\n\nexport function HBoxWidget({ modelId, className }: WidgetComponentProps) {\n  // Subscribe to individual state keys\n  const children = useWidgetModelValue<string[]>(modelId, \"children\");\n  const boxStyle = useWidgetModelValue<string>(modelId, \"box_style\") ?? \"\";\n\n  const styleClass = BOX_STYLE_MAP[boxStyle] ?? \"\";\n\n  return (\n    <div\n      className={cn(\n        \"flex flex-row flex-wrap items-baseline gap-1\",\n        styleClass,\n        className,\n      )}\n      data-widget-id={modelId}\n      data-widget-type=\"HBox\"\n    >\n      {children?.map((childRef) => {\n        const childId = parseModelRef(childRef);\n        return childId ? <WidgetView key={childId} modelId={childId} /> : null;\n      })}\n    </div>\n  );\n}\n\nexport default HBoxWidget;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/hbox-widget.tsx"
    },
    {
      "path": "registry/widgets/controls/box-widget.tsx",
      "content": "/**\n * Box widget - generic flex container.\n *\n * Maps to ipywidgets BoxModel. Base container for layout widgets.\n * Defaults to vertical stacking (like VBox).\n */\n\nimport { cn } from \"@/lib/utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport { parseModelRef, useWidgetModelValue } from \"../widget-store-context\";\nimport { WidgetView } from \"../widget-view\";\n\n// Map ipywidgets box_style to Tailwind classes\nconst BOX_STYLE_MAP: Record<string, string> = {\n  \"\": \"\",\n  primary:\n    \"border border-blue-500 bg-blue-50/50 dark:bg-blue-950/50 rounded-md p-2\",\n  success:\n    \"border border-green-500 bg-green-50/50 dark:bg-green-950/50 rounded-md p-2\",\n  info: \"border border-sky-500 bg-sky-50/50 dark:bg-sky-950/50 rounded-md p-2\",\n  warning:\n    \"border border-yellow-500 bg-yellow-50/50 dark:bg-yellow-950/50 rounded-md p-2\",\n  danger:\n    \"border border-red-500 bg-red-50/50 dark:bg-red-950/50 rounded-md p-2\",\n};\n\nexport function BoxWidget({ modelId, className }: WidgetComponentProps) {\n  // Subscribe to individual state keys\n  const children = useWidgetModelValue<string[]>(modelId, \"children\");\n  const boxStyle = useWidgetModelValue<string>(modelId, \"box_style\") ?? \"\";\n\n  const styleClass = BOX_STYLE_MAP[boxStyle] ?? \"\";\n\n  return (\n    <div\n      className={cn(\"flex flex-col gap-2\", styleClass, className)}\n      data-widget-id={modelId}\n      data-widget-type=\"Box\"\n    >\n      {children?.map((childRef) => {\n        const childId = parseModelRef(childRef);\n        return childId ? <WidgetView key={childId} modelId={childId} /> : null;\n      })}\n    </div>\n  );\n}\n\nexport default BoxWidget;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/box-widget.tsx"
    },
    {
      "path": "registry/widgets/controls/gridbox-widget.tsx",
      "content": "/**\n * GridBox widget - CSS grid container.\n *\n * Maps to ipywidgets GridBoxModel. Arranges children in a CSS grid layout.\n * Default is a responsive 2-column grid.\n */\n\nimport { cn } from \"@/lib/utils\";\nimport { useLayoutStyles } from \"../use-layout-styles\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport { parseModelRef, useWidgetModelValue } from \"../widget-store-context\";\nimport { WidgetView } from \"../widget-view\";\n\n// Map ipywidgets box_style to Tailwind classes\nconst BOX_STYLE_MAP: Record<string, string> = {\n  \"\": \"\",\n  primary:\n    \"border border-blue-500 bg-blue-50/50 dark:bg-blue-950/50 rounded-md p-2\",\n  success:\n    \"border border-green-500 bg-green-50/50 dark:bg-green-950/50 rounded-md p-2\",\n  info: \"border border-sky-500 bg-sky-50/50 dark:bg-sky-950/50 rounded-md p-2\",\n  warning:\n    \"border border-yellow-500 bg-yellow-50/50 dark:bg-yellow-950/50 rounded-md p-2\",\n  danger:\n    \"border border-red-500 bg-red-50/50 dark:bg-red-950/50 rounded-md p-2\",\n};\n\nexport function GridBoxWidget({ modelId, className }: WidgetComponentProps) {\n  // Subscribe to individual state keys\n  const children = useWidgetModelValue<string[]>(modelId, \"children\");\n  const boxStyle = useWidgetModelValue<string>(modelId, \"box_style\") ?? \"\";\n\n  // Get layout styles from the Layout model\n  const { containerStyle, hasGridLayout } = useLayoutStyles(modelId);\n\n  const styleClass = BOX_STYLE_MAP[boxStyle] ?? \"\";\n\n  return (\n    <div\n      className={cn(\n        \"grid\",\n        // Only apply default Tailwind grid classes if no layout grid is specified\n        !hasGridLayout && \"grid-cols-1 sm:grid-cols-2 gap-2\",\n        styleClass,\n        className,\n      )}\n      style={containerStyle}\n      data-widget-id={modelId}\n      data-widget-type=\"GridBox\"\n    >\n      {children?.map((childRef) => {\n        const childId = parseModelRef(childRef);\n        return childId ? <WidgetView key={childId} modelId={childId} /> : null;\n      })}\n    </div>\n  );\n}\n\nexport default GridBoxWidget;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/gridbox-widget.tsx"
    },
    {
      "path": "registry/widgets/controls/accordion-widget.tsx",
      "content": "/**\n * Accordion widget - collapsible panel container.\n *\n * Maps to ipywidgets AccordionModel. Displays children as collapsible panels.\n */\n\nimport {\n  Accordion,\n  AccordionContent,\n  AccordionItem,\n  AccordionTrigger,\n} from \"@/components/ui/accordion\";\nimport { cn } from \"@/lib/utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport {\n  parseModelRef,\n  useWidgetModelValue,\n  useWidgetStoreRequired,\n} from \"../widget-store-context\";\nimport { WidgetView } from \"../widget-view\";\n\nexport function AccordionWidget({ modelId, className }: WidgetComponentProps) {\n  const { sendUpdate } = useWidgetStoreRequired();\n\n  // Subscribe to individual state keys\n  const children = useWidgetModelValue<string[]>(modelId, \"children\");\n  const titles = useWidgetModelValue<string[]>(modelId, \"_titles\") ?? [];\n  const selectedIndex = useWidgetModelValue<number | null>(\n    modelId,\n    \"selected_index\",\n  );\n\n  const handleValueChange = (value: string) => {\n    // Convert string value back to index (or null if collapsed)\n    const index = value !== \"\" ? parseInt(value, 10) : null;\n    sendUpdate(modelId, { selected_index: index });\n  };\n\n  return (\n    <Accordion\n      type=\"single\"\n      collapsible\n      value={selectedIndex !== null ? String(selectedIndex) : \"\"}\n      onValueChange={handleValueChange}\n      className={cn(\"w-full\", className)}\n      data-widget-id={modelId}\n      data-widget-type=\"Accordion\"\n    >\n      {children?.map((childRef, index) => {\n        const childId = parseModelRef(childRef);\n        if (!childId) return null;\n\n        return (\n          <AccordionItem key={childId} value={String(index)}>\n            <AccordionTrigger>\n              {titles[index] ?? `Panel ${index + 1}`}\n            </AccordionTrigger>\n            <AccordionContent>\n              <WidgetView modelId={childId} />\n            </AccordionContent>\n          </AccordionItem>\n        );\n      })}\n    </Accordion>\n  );\n}\n\nexport default AccordionWidget;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/accordion-widget.tsx"
    },
    {
      "path": "registry/widgets/controls/tab-widget.tsx",
      "content": "/**\n * Tab widget - tabbed panel container.\n *\n * Maps to ipywidgets TabModel. Displays children as tabbed panels.\n */\n\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@/components/ui/tabs\";\nimport { cn } from \"@/lib/utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport {\n  parseModelRef,\n  useWidgetModelValue,\n  useWidgetStoreRequired,\n} from \"../widget-store-context\";\nimport { WidgetView } from \"../widget-view\";\n\nexport function TabWidget({ modelId, className }: WidgetComponentProps) {\n  const { sendUpdate } = useWidgetStoreRequired();\n\n  // Subscribe to individual state keys\n  const children = useWidgetModelValue<string[]>(modelId, \"children\");\n  const titles = useWidgetModelValue<string[]>(modelId, \"_titles\") ?? [];\n  const selectedIndex =\n    useWidgetModelValue<number>(modelId, \"selected_index\") ?? 0;\n\n  const handleValueChange = (value: string) => {\n    sendUpdate(modelId, { selected_index: parseInt(value, 10) });\n  };\n\n  return (\n    <Tabs\n      value={String(selectedIndex)}\n      onValueChange={handleValueChange}\n      className={cn(\"w-full\", className)}\n      data-widget-id={modelId}\n      data-widget-type=\"Tab\"\n    >\n      <TabsList>\n        {children?.map((_, index) => (\n          <TabsTrigger key={index} value={String(index)}>\n            {titles[index] ?? `Tab ${index + 1}`}\n          </TabsTrigger>\n        ))}\n      </TabsList>\n      {children?.map((childRef, index) => {\n        const childId = parseModelRef(childRef);\n        if (!childId) return null;\n\n        return (\n          <TabsContent key={childId} value={String(index)}>\n            <WidgetView modelId={childId} />\n          </TabsContent>\n        );\n      })}\n    </Tabs>\n  );\n}\n\nexport default TabWidget;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/tab-widget.tsx"
    },
    {
      "path": "registry/widgets/controls/label-widget.tsx",
      "content": "/**\n * Label widget - renders plain text content.\n *\n * Maps to ipywidgets LabelModel.\n */\n\nimport { Label } from \"@/components/ui/label\";\nimport { cn } from \"@/lib/utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport { useWidgetModelValue } from \"../widget-store-context\";\n\nexport function LabelWidget({ modelId, className }: WidgetComponentProps) {\n  const value = useWidgetModelValue<string>(modelId, \"value\") ?? \"\";\n  const description = useWidgetModelValue<string>(modelId, \"description\");\n  const placeholder = useWidgetModelValue<string>(modelId, \"placeholder\");\n\n  // Show placeholder if value is empty\n  const displayValue = value || placeholder || \"\";\n\n  return (\n    <div\n      className={cn(\"inline-flex shrink-0 items-baseline gap-1\", className)}\n      data-widget-id={modelId}\n      data-widget-type=\"Label\"\n    >\n      {description && <Label className=\"shrink-0 text-sm\">{description}</Label>}\n      <span className=\"widget-label-content\">{displayValue}</span>\n    </div>\n  );\n}\n\nexport default LabelWidget;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/label-widget.tsx"
    },
    {
      "path": "registry/widgets/controls/link-widget.tsx",
      "content": "/**\n * Link widgets — frontend-only property synchronization.\n *\n * LinkModel: bidirectional sync (widgets.jslink)\n * DirectionalLinkModel: one-way sync (widgets.jsdlink)\n *\n * These are headless widgets that render no visible UI. Property\n * synchronization is handled at the store level by createLinkManager\n * (see link-subscriptions.ts), which runs automatically in the\n * WidgetStoreProvider.\n *\n * These components exist in the widget registry so that WidgetView\n * renders null instead of showing \"Unsupported widget\".\n */\n\nimport type { WidgetComponentProps } from \"../widget-registry\";\n\n/**\n * DirectionalLinkWidget — one-way property synchronization (source → target).\n * Renders nothing. Sync logic runs at the store level.\n */\nexport function DirectionalLinkWidget(_props: WidgetComponentProps) {\n  return null;\n}\n\n/**\n * LinkWidget — bidirectional property synchronization (source ↔ target).\n * Renders nothing. Sync logic runs at the store level.\n */\nexport function LinkWidget(_props: WidgetComponentProps) {\n  return null;\n}\n",
      "type": "registry:component",
      "target": "components/widgets/controls/link-widget.tsx"
    },
    {
      "path": "registry/widgets/controls/image-widget.tsx",
      "content": "/**\n * Image widget - displays images from the kernel.\n *\n * Maps to ipywidgets ImageModel.\n */\n\nimport { Label } from \"@/components/ui/label\";\nimport { cn } from \"@/lib/utils\";\nimport { buildMediaSrc } from \"../buffer-utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport { useWidgetModelValue } from \"../widget-store-context\";\n\nexport function ImageWidget({ modelId, className }: WidgetComponentProps) {\n  const value = useWidgetModelValue<string | ArrayBuffer>(modelId, \"value\");\n  const format = useWidgetModelValue<string>(modelId, \"format\") ?? \"png\";\n  const width = useWidgetModelValue<string>(modelId, \"width\") ?? \"\";\n  const height = useWidgetModelValue<string>(modelId, \"height\") ?? \"\";\n  const description = useWidgetModelValue<string>(modelId, \"description\");\n\n  const src = buildMediaSrc(value, \"image\", format);\n\n  if (!src) {\n    return null;\n  }\n\n  // Build style object for width/height\n  const style: React.CSSProperties = {};\n  if (width) style.width = width;\n  if (height) style.height = height;\n\n  return (\n    <div\n      className={cn(\"inline-flex items-start gap-3\", className)}\n      data-widget-id={modelId}\n      data-widget-type=\"Image\"\n    >\n      {description && (\n        <Label className=\"shrink-0 pt-1 text-sm\">{description}</Label>\n      )}\n      <img\n        src={src}\n        alt={description || \"Widget image\"}\n        className=\"block max-w-full h-auto\"\n        style={{ objectFit: \"contain\", ...style }}\n      />\n    </div>\n  );\n}\n\nexport default ImageWidget;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/image-widget.tsx"
    },
    {
      "path": "registry/widgets/controls/int-text-widget.tsx",
      "content": "\"use client\";\n\n/**\n * IntText widget - renders a numeric text input for integers.\n *\n * Maps to ipywidgets IntTextModel.\n */\n\nimport { useCallback, useEffect, useState } from \"react\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { cn } from \"@/lib/utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport {\n  useWidgetModelValue,\n  useWidgetStoreRequired,\n} from \"../widget-store-context\";\n\nexport function IntTextWidget({ modelId, className }: WidgetComponentProps) {\n  const { sendUpdate } = useWidgetStoreRequired();\n\n  // Subscribe to individual state keys\n  const value = useWidgetModelValue<number>(modelId, \"value\") ?? 0;\n  const min = useWidgetModelValue<number>(modelId, \"min\");\n  const max = useWidgetModelValue<number>(modelId, \"max\");\n  const step = useWidgetModelValue<number>(modelId, \"step\") ?? 1;\n  const description = useWidgetModelValue<string>(modelId, \"description\");\n  const disabled = useWidgetModelValue<boolean>(modelId, \"disabled\") ?? false;\n  const continuousUpdate =\n    useWidgetModelValue<boolean>(modelId, \"continuous_update\") ?? false;\n\n  // Local state for the input value (as string for editing)\n  const [localValue, setLocalValue] = useState(String(value));\n\n  // Sync local state when value changes from kernel\n  useEffect(() => {\n    setLocalValue(String(value));\n  }, [value]);\n\n  const clampValue = useCallback(\n    (val: number): number => {\n      let clamped = val;\n      if (min !== undefined && min !== null) {\n        clamped = Math.max(min, clamped);\n      }\n      if (max !== undefined && max !== null) {\n        clamped = Math.min(max, clamped);\n      }\n      return clamped;\n    },\n    [min, max],\n  );\n\n  const handleChange = useCallback(\n    (e: React.ChangeEvent<HTMLInputElement>) => {\n      const newValue = e.target.value;\n      setLocalValue(newValue);\n\n      if (continuousUpdate) {\n        const parsed = parseInt(newValue, 10);\n        if (!Number.isNaN(parsed)) {\n          const clamped = clampValue(parsed);\n          sendUpdate(modelId, { value: clamped });\n        }\n      }\n    },\n    [modelId, continuousUpdate, clampValue, sendUpdate],\n  );\n\n  const handleBlur = useCallback(() => {\n    const parsed = parseInt(localValue, 10);\n    if (!Number.isNaN(parsed)) {\n      const clamped = clampValue(parsed);\n      setLocalValue(String(clamped));\n      if (clamped !== value) {\n        sendUpdate(modelId, { value: clamped });\n      }\n    } else {\n      // Reset to current value if invalid\n      setLocalValue(String(value));\n    }\n  }, [modelId, localValue, value, clampValue, sendUpdate]);\n\n  const handleKeyDown = useCallback(\n    (e: React.KeyboardEvent<HTMLInputElement>) => {\n      if (e.key === \"Enter\") {\n        const parsed = parseInt(localValue, 10);\n        if (!Number.isNaN(parsed)) {\n          const clamped = clampValue(parsed);\n          setLocalValue(String(clamped));\n          sendUpdate(modelId, { value: clamped });\n        }\n      }\n    },\n    [modelId, localValue, clampValue, sendUpdate],\n  );\n\n  return (\n    <div\n      className={cn(\"flex items-center gap-3\", className)}\n      data-widget-id={modelId}\n      data-widget-type=\"IntText\"\n    >\n      {description && <Label className=\"shrink-0 text-sm\">{description}</Label>}\n      <Input\n        type=\"number\"\n        value={localValue}\n        disabled={disabled}\n        step={step}\n        min={min}\n        max={max}\n        onChange={handleChange}\n        onBlur={handleBlur}\n        onKeyDown={handleKeyDown}\n        className=\"w-24\"\n      />\n    </div>\n  );\n}\n\nexport default IntTextWidget;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/int-text-widget.tsx"
    },
    {
      "path": "registry/widgets/controls/float-text-widget.tsx",
      "content": "\"use client\";\n\n/**\n * FloatText widget - renders a numeric text input for floats.\n *\n * Maps to ipywidgets FloatTextModel.\n */\n\nimport { useCallback, useEffect, useState } from \"react\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { cn } from \"@/lib/utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport {\n  useWidgetModelValue,\n  useWidgetStoreRequired,\n} from \"../widget-store-context\";\n\nexport function FloatTextWidget({ modelId, className }: WidgetComponentProps) {\n  const { sendUpdate } = useWidgetStoreRequired();\n\n  // Subscribe to individual state keys\n  const value = useWidgetModelValue<number>(modelId, \"value\") ?? 0;\n  const min = useWidgetModelValue<number>(modelId, \"min\");\n  const max = useWidgetModelValue<number>(modelId, \"max\");\n  const step = useWidgetModelValue<number>(modelId, \"step\") ?? 0.1;\n  const description = useWidgetModelValue<string>(modelId, \"description\");\n  const disabled = useWidgetModelValue<boolean>(modelId, \"disabled\") ?? false;\n  const continuousUpdate =\n    useWidgetModelValue<boolean>(modelId, \"continuous_update\") ?? false;\n\n  // Local state for the input value (as string for editing)\n  const [localValue, setLocalValue] = useState(String(value));\n\n  // Sync local state when value changes from kernel\n  useEffect(() => {\n    setLocalValue(String(value));\n  }, [value]);\n\n  const clampValue = useCallback(\n    (val: number): number => {\n      let clamped = val;\n      if (min !== undefined && min !== null) {\n        clamped = Math.max(min, clamped);\n      }\n      if (max !== undefined && max !== null) {\n        clamped = Math.min(max, clamped);\n      }\n      return clamped;\n    },\n    [min, max],\n  );\n\n  const handleChange = useCallback(\n    (e: React.ChangeEvent<HTMLInputElement>) => {\n      const newValue = e.target.value;\n      setLocalValue(newValue);\n\n      if (continuousUpdate) {\n        const parsed = parseFloat(newValue);\n        if (!Number.isNaN(parsed)) {\n          const clamped = clampValue(parsed);\n          sendUpdate(modelId, { value: clamped });\n        }\n      }\n    },\n    [modelId, continuousUpdate, clampValue, sendUpdate],\n  );\n\n  const handleBlur = useCallback(() => {\n    const parsed = parseFloat(localValue);\n    if (!Number.isNaN(parsed)) {\n      const clamped = clampValue(parsed);\n      setLocalValue(String(clamped));\n      if (clamped !== value) {\n        sendUpdate(modelId, { value: clamped });\n      }\n    } else {\n      // Reset to current value if invalid\n      setLocalValue(String(value));\n    }\n  }, [modelId, localValue, value, clampValue, sendUpdate]);\n\n  const handleKeyDown = useCallback(\n    (e: React.KeyboardEvent<HTMLInputElement>) => {\n      if (e.key === \"Enter\") {\n        const parsed = parseFloat(localValue);\n        if (!Number.isNaN(parsed)) {\n          const clamped = clampValue(parsed);\n          setLocalValue(String(clamped));\n          sendUpdate(modelId, { value: clamped });\n        }\n      }\n    },\n    [modelId, localValue, clampValue, sendUpdate],\n  );\n\n  return (\n    <div\n      className={cn(\"flex items-center gap-3\", className)}\n      data-widget-id={modelId}\n      data-widget-type=\"FloatText\"\n    >\n      {description && <Label className=\"shrink-0 text-sm\">{description}</Label>}\n      <Input\n        type=\"number\"\n        value={localValue}\n        disabled={disabled}\n        step={step}\n        min={min}\n        max={max}\n        onChange={handleChange}\n        onBlur={handleBlur}\n        onKeyDown={handleKeyDown}\n        className=\"w-24\"\n      />\n    </div>\n  );\n}\n\nexport default FloatTextWidget;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/float-text-widget.tsx"
    },
    {
      "path": "registry/widgets/controls/bounded-int-text-widget.tsx",
      "content": "\"use client\";\n\n/**\n * BoundedIntText widget - renders a bounded numeric text input for integers.\n *\n * Maps to ipywidgets BoundedIntTextModel.\n * This is functionally the same as IntText since both support min/max bounds.\n */\n\nimport { useCallback, useEffect, useState } from \"react\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { cn } from \"@/lib/utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport {\n  useWidgetModelValue,\n  useWidgetStoreRequired,\n} from \"../widget-store-context\";\n\nexport function BoundedIntTextWidget({\n  modelId,\n  className,\n}: WidgetComponentProps) {\n  const { sendUpdate } = useWidgetStoreRequired();\n\n  // Subscribe to individual state keys\n  const value = useWidgetModelValue<number>(modelId, \"value\") ?? 0;\n  const min = useWidgetModelValue<number>(modelId, \"min\") ?? 0;\n  const max = useWidgetModelValue<number>(modelId, \"max\") ?? 100;\n  const step = useWidgetModelValue<number>(modelId, \"step\") ?? 1;\n  const description = useWidgetModelValue<string>(modelId, \"description\");\n  const disabled = useWidgetModelValue<boolean>(modelId, \"disabled\") ?? false;\n  const continuousUpdate =\n    useWidgetModelValue<boolean>(modelId, \"continuous_update\") ?? false;\n\n  // Local state for the input value (as string for editing)\n  const [localValue, setLocalValue] = useState(String(value));\n\n  // Sync local state when value changes from kernel\n  useEffect(() => {\n    setLocalValue(String(value));\n  }, [value]);\n\n  const clampValue = useCallback(\n    (val: number): number => {\n      return Math.max(min, Math.min(max, val));\n    },\n    [min, max],\n  );\n\n  const handleChange = useCallback(\n    (e: React.ChangeEvent<HTMLInputElement>) => {\n      const newValue = e.target.value;\n      setLocalValue(newValue);\n\n      if (continuousUpdate) {\n        const parsed = parseInt(newValue, 10);\n        if (!Number.isNaN(parsed)) {\n          const clamped = clampValue(parsed);\n          sendUpdate(modelId, { value: clamped });\n        }\n      }\n    },\n    [modelId, continuousUpdate, clampValue, sendUpdate],\n  );\n\n  const handleBlur = useCallback(() => {\n    const parsed = parseInt(localValue, 10);\n    if (!Number.isNaN(parsed)) {\n      const clamped = clampValue(parsed);\n      setLocalValue(String(clamped));\n      if (clamped !== value) {\n        sendUpdate(modelId, { value: clamped });\n      }\n    } else {\n      // Reset to current value if invalid\n      setLocalValue(String(value));\n    }\n  }, [modelId, localValue, value, clampValue, sendUpdate]);\n\n  const handleKeyDown = useCallback(\n    (e: React.KeyboardEvent<HTMLInputElement>) => {\n      if (e.key === \"Enter\") {\n        const parsed = parseInt(localValue, 10);\n        if (!Number.isNaN(parsed)) {\n          const clamped = clampValue(parsed);\n          setLocalValue(String(clamped));\n          sendUpdate(modelId, { value: clamped });\n        }\n      }\n    },\n    [modelId, localValue, clampValue, sendUpdate],\n  );\n\n  return (\n    <div\n      className={cn(\"flex items-center gap-3\", className)}\n      data-widget-id={modelId}\n      data-widget-type=\"BoundedIntText\"\n    >\n      {description && <Label className=\"shrink-0 text-sm\">{description}</Label>}\n      <Input\n        type=\"number\"\n        value={localValue}\n        disabled={disabled}\n        step={step}\n        min={min}\n        max={max}\n        onChange={handleChange}\n        onBlur={handleBlur}\n        onKeyDown={handleKeyDown}\n        className=\"w-24\"\n      />\n    </div>\n  );\n}\n\nexport default BoundedIntTextWidget;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/bounded-int-text-widget.tsx"
    },
    {
      "path": "registry/widgets/controls/bounded-float-text-widget.tsx",
      "content": "\"use client\";\n\n/**\n * BoundedFloatText widget - renders a bounded numeric text input for floats.\n *\n * Maps to ipywidgets BoundedFloatTextModel.\n * This is functionally the same as FloatText since both support min/max bounds.\n */\n\nimport { useCallback, useEffect, useState } from \"react\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { cn } from \"@/lib/utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport {\n  useWidgetModelValue,\n  useWidgetStoreRequired,\n} from \"../widget-store-context\";\n\nexport function BoundedFloatTextWidget({\n  modelId,\n  className,\n}: WidgetComponentProps) {\n  const { sendUpdate } = useWidgetStoreRequired();\n\n  // Subscribe to individual state keys\n  const value = useWidgetModelValue<number>(modelId, \"value\") ?? 0;\n  const min = useWidgetModelValue<number>(modelId, \"min\") ?? 0;\n  const max = useWidgetModelValue<number>(modelId, \"max\") ?? 100;\n  const step = useWidgetModelValue<number>(modelId, \"step\") ?? 0.1;\n  const description = useWidgetModelValue<string>(modelId, \"description\");\n  const disabled = useWidgetModelValue<boolean>(modelId, \"disabled\") ?? false;\n  const continuousUpdate =\n    useWidgetModelValue<boolean>(modelId, \"continuous_update\") ?? false;\n\n  // Local state for the input value (as string for editing)\n  const [localValue, setLocalValue] = useState(String(value));\n\n  // Sync local state when value changes from kernel\n  useEffect(() => {\n    setLocalValue(String(value));\n  }, [value]);\n\n  const clampValue = useCallback(\n    (val: number): number => {\n      return Math.max(min, Math.min(max, val));\n    },\n    [min, max],\n  );\n\n  const handleChange = useCallback(\n    (e: React.ChangeEvent<HTMLInputElement>) => {\n      const newValue = e.target.value;\n      setLocalValue(newValue);\n\n      if (continuousUpdate) {\n        const parsed = parseFloat(newValue);\n        if (!Number.isNaN(parsed)) {\n          const clamped = clampValue(parsed);\n          sendUpdate(modelId, { value: clamped });\n        }\n      }\n    },\n    [modelId, continuousUpdate, clampValue, sendUpdate],\n  );\n\n  const handleBlur = useCallback(() => {\n    const parsed = parseFloat(localValue);\n    if (!Number.isNaN(parsed)) {\n      const clamped = clampValue(parsed);\n      setLocalValue(String(clamped));\n      if (clamped !== value) {\n        sendUpdate(modelId, { value: clamped });\n      }\n    } else {\n      // Reset to current value if invalid\n      setLocalValue(String(value));\n    }\n  }, [modelId, localValue, value, clampValue, sendUpdate]);\n\n  const handleKeyDown = useCallback(\n    (e: React.KeyboardEvent<HTMLInputElement>) => {\n      if (e.key === \"Enter\") {\n        const parsed = parseFloat(localValue);\n        if (!Number.isNaN(parsed)) {\n          const clamped = clampValue(parsed);\n          setLocalValue(String(clamped));\n          sendUpdate(modelId, { value: clamped });\n        }\n      }\n    },\n    [modelId, localValue, clampValue, sendUpdate],\n  );\n\n  return (\n    <div\n      className={cn(\"flex items-center gap-3\", className)}\n      data-widget-id={modelId}\n      data-widget-type=\"BoundedFloatText\"\n    >\n      {description && <Label className=\"shrink-0 text-sm\">{description}</Label>}\n      <Input\n        type=\"number\"\n        value={localValue}\n        disabled={disabled}\n        step={step}\n        min={min}\n        max={max}\n        onChange={handleChange}\n        onBlur={handleBlur}\n        onKeyDown={handleKeyDown}\n        className=\"w-24\"\n      />\n    </div>\n  );\n}\n\nexport default BoundedFloatTextWidget;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/bounded-float-text-widget.tsx"
    },
    {
      "path": "registry/widgets/controls/password-widget.tsx",
      "content": "\"use client\";\n\n/**\n * Password widget - renders a masked text input field.\n *\n * Maps to ipywidgets PasswordModel.\n */\n\nimport { useCallback, useEffect, useState } from \"react\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { cn } from \"@/lib/utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport {\n  useWidgetModelValue,\n  useWidgetStoreRequired,\n} from \"../widget-store-context\";\n\nexport function PasswordWidget({ modelId, className }: WidgetComponentProps) {\n  const { sendUpdate, sendCustom } = useWidgetStoreRequired();\n\n  // Subscribe to individual state keys\n  const value = useWidgetModelValue<string>(modelId, \"value\") ?? \"\";\n  const description = useWidgetModelValue<string>(modelId, \"description\");\n  const placeholder = useWidgetModelValue<string>(modelId, \"placeholder\") ?? \"\";\n  const disabled = useWidgetModelValue<boolean>(modelId, \"disabled\") ?? false;\n  const continuousUpdate =\n    useWidgetModelValue<boolean>(modelId, \"continuous_update\") ?? true;\n\n  // Local state for non-continuous updates\n  const [localValue, setLocalValue] = useState(value);\n\n  // Sync local state when value changes from kernel\n  useEffect(() => {\n    setLocalValue(value);\n  }, [value]);\n\n  const handleChange = useCallback(\n    (e: React.ChangeEvent<HTMLInputElement>) => {\n      const newValue = e.target.value;\n      setLocalValue(newValue);\n\n      if (continuousUpdate) {\n        sendUpdate(modelId, { value: newValue });\n      }\n    },\n    [modelId, continuousUpdate, sendUpdate],\n  );\n\n  const handleBlur = useCallback(() => {\n    if (!continuousUpdate && localValue !== value) {\n      sendUpdate(modelId, { value: localValue });\n    }\n  }, [modelId, continuousUpdate, localValue, value, sendUpdate]);\n\n  const handleKeyDown = useCallback(\n    (e: React.KeyboardEvent<HTMLInputElement>) => {\n      if (e.key === \"Enter\") {\n        // Send submit event\n        sendCustom(modelId, { event: \"submit\" });\n        // Also ensure value is synced\n        if (!continuousUpdate && localValue !== value) {\n          sendUpdate(modelId, { value: localValue });\n        }\n      }\n    },\n    [modelId, continuousUpdate, localValue, value, sendUpdate, sendCustom],\n  );\n\n  return (\n    <div\n      className={cn(\"flex items-center gap-3\", className)}\n      data-widget-id={modelId}\n      data-widget-type=\"Password\"\n    >\n      {description && <Label className=\"shrink-0 text-sm\">{description}</Label>}\n      <Input\n        type=\"password\"\n        value={localValue}\n        placeholder={placeholder}\n        disabled={disabled}\n        onChange={handleChange}\n        onBlur={handleBlur}\n        onKeyDown={handleKeyDown}\n        className=\"flex-1\"\n      />\n    </div>\n  );\n}\n\nexport default PasswordWidget;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/password-widget.tsx"
    },
    {
      "path": "registry/widgets/controls/valid-widget.tsx",
      "content": "/**\n * Valid widget - displays a validation indicator (checkmark or X).\n *\n * Maps to ipywidgets ValidModel.\n */\n\nimport { CheckIcon, XIcon } from \"lucide-react\";\nimport { Label } from \"@/components/ui/label\";\nimport { cn } from \"@/lib/utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport { useWidgetModelValue } from \"../widget-store-context\";\n\nexport function ValidWidget({ modelId, className }: WidgetComponentProps) {\n  const value = useWidgetModelValue<boolean>(modelId, \"value\") ?? false;\n  const readout = useWidgetModelValue<string>(modelId, \"readout\") ?? \"\";\n  const description = useWidgetModelValue<string>(modelId, \"description\");\n\n  return (\n    <div\n      className={cn(\"inline-flex items-center gap-2\", className)}\n      data-widget-id={modelId}\n      data-widget-type=\"Valid\"\n    >\n      {description && <Label className=\"shrink-0 text-sm\">{description}</Label>}\n      {value ? (\n        <CheckIcon className=\"size-4 text-green-500\" />\n      ) : (\n        <XIcon className=\"size-4 text-red-500\" />\n      )}\n      {!value && readout && (\n        <span className=\"text-sm text-muted-foreground\">{readout}</span>\n      )}\n    </div>\n  );\n}\n\nexport default ValidWidget;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/valid-widget.tsx"
    },
    {
      "path": "registry/widgets/controls/select-widget.tsx",
      "content": "/**\n * Select widget - renders a single-selection listbox.\n *\n * Maps to ipywidgets SelectModel.\n */\n\nimport { Label } from \"@/components/ui/label\";\nimport { cn } from \"@/lib/utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport {\n  useWidgetModelValue,\n  useWidgetStoreRequired,\n} from \"../widget-store-context\";\n\nexport function SelectWidget({ modelId, className }: WidgetComponentProps) {\n  const { sendUpdate } = useWidgetStoreRequired();\n\n  // Subscribe to individual state keys\n  const options =\n    useWidgetModelValue<string[]>(modelId, \"_options_labels\") ?? [];\n  const selectedIndex = useWidgetModelValue<number | null>(modelId, \"index\");\n  const description = useWidgetModelValue<string>(modelId, \"description\");\n  const disabled = useWidgetModelValue<boolean>(modelId, \"disabled\") ?? false;\n  const rows = useWidgetModelValue<number>(modelId, \"rows\") ?? 5;\n\n  const handleSelect = (idx: number) => {\n    if (disabled) return;\n    sendUpdate(modelId, { index: idx });\n  };\n\n  // Calculate height based on rows\n  const itemHeight = 32; // approximate height per item in px\n  const maxHeight = rows * itemHeight;\n\n  return (\n    <div\n      className={cn(\"flex items-start gap-3\", className)}\n      data-widget-id={modelId}\n      data-widget-type=\"Select\"\n    >\n      {description && (\n        <Label className=\"shrink-0 pt-1 text-sm\">{description}</Label>\n      )}\n      <div\n        role=\"listbox\"\n        aria-disabled={disabled}\n        className={cn(\n          \"w-48 overflow-y-auto rounded-md border border-input bg-background shadow-xs\",\n          disabled && \"cursor-not-allowed opacity-50\",\n        )}\n        style={{ maxHeight }}\n      >\n        {options.map((option, idx) => {\n          const isSelected = selectedIndex === idx;\n          return (\n            <div\n              key={idx}\n              role=\"option\"\n              aria-selected={isSelected}\n              onClick={() => handleSelect(idx)}\n              className={cn(\n                \"flex cursor-pointer select-none items-center px-3 py-1.5 text-sm\",\n                \"hover:bg-accent hover:text-accent-foreground\",\n                isSelected && \"bg-accent text-accent-foreground\",\n                disabled && \"pointer-events-none\",\n              )}\n            >\n              <span>{option}</span>\n            </div>\n          );\n        })}\n      </div>\n    </div>\n  );\n}\n\nexport default SelectWidget;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/select-widget.tsx"
    },
    {
      "path": "registry/widgets/controls/html-math-widget.tsx",
      "content": "/**\n * HTMLMath widget - renders HTML content with LaTeX math support.\n *\n * Maps to ipywidgets HTMLMathModel.\n * Uses KaTeX to render math expressions in $...$ and $$...$$ delimiters.\n */\n\nimport \"katex/dist/katex.min.css\";\n\nimport katex from \"katex\";\nimport { Label } from \"@/components/ui/label\";\nimport { cn } from \"@/lib/utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport { useWidgetModelValue } from \"../widget-store-context\";\n\n// Process math in content using KaTeX\nfunction processMath(content: string): string {\n  // Process display math ($$...$$)\n  let processed = content.replace(/\\$\\$([\\s\\S]*?)\\$\\$/g, (_, math) => {\n    try {\n      return katex.renderToString(math.trim(), {\n        displayMode: true,\n        throwOnError: false,\n      });\n    } catch {\n      return `$$${math}$$`;\n    }\n  });\n\n  // Process inline math ($...$)\n  processed = processed.replace(/\\$([^$\\n]+?)\\$/g, (_, math) => {\n    try {\n      return katex.renderToString(math.trim(), {\n        displayMode: false,\n        throwOnError: false,\n      });\n    } catch {\n      return `$${math}$`;\n    }\n  });\n\n  return processed;\n}\n\nexport function HTMLMathWidget({ modelId, className }: WidgetComponentProps) {\n  const value = useWidgetModelValue<string>(modelId, \"value\") ?? \"\";\n  const description = useWidgetModelValue<string>(modelId, \"description\");\n  const placeholder = useWidgetModelValue<string>(modelId, \"placeholder\");\n\n  // Show placeholder if value is empty\n  const displayValue = value || placeholder || \"\";\n\n  // Process content for math rendering\n  const processedContent = displayValue ? processMath(displayValue) : \"\";\n\n  return (\n    <div\n      className={cn(\"inline-flex shrink-0 items-baseline gap-1\", className)}\n      data-widget-id={modelId}\n      data-widget-type=\"HTMLMath\"\n    >\n      {description && <Label className=\"shrink-0 text-sm\">{description}</Label>}\n      <div\n        className=\"widget-html-math-content\"\n        dangerouslySetInnerHTML={{ __html: processedContent }}\n      />\n    </div>\n  );\n}\n\nexport default HTMLMathWidget;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/html-math-widget.tsx"
    },
    {
      "path": "registry/widgets/controls/date-picker-widget.tsx",
      "content": "\"use client\";\n\n/**\n * DatePicker widget - renders a date input field.\n *\n * Maps to ipywidgets DatePickerModel.\n */\n\nimport { useCallback } from \"react\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { cn } from \"@/lib/utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport {\n  useWidgetModelValue,\n  useWidgetStoreRequired,\n} from \"../widget-store-context\";\n\ntype IpyDate = { year: number; month: number; date: number } | null;\n\n// Convert ipywidgets date object to YYYY-MM-DD format for input\nfunction toDateString(value: IpyDate): string {\n  if (!value) return \"\";\n  const year = String(value.year).padStart(4, \"0\");\n  const month = String(value.month + 1).padStart(2, \"0\");\n  const date = String(value.date).padStart(2, \"0\");\n  return `${year}-${month}-${date}`;\n}\n\nexport function DatePickerWidget({ modelId, className }: WidgetComponentProps) {\n  const { sendUpdate } = useWidgetStoreRequired();\n\n  // Subscribe to individual state keys\n  const value = useWidgetModelValue<IpyDate>(modelId, \"value\") ?? null;\n  const min = useWidgetModelValue<IpyDate>(modelId, \"min\") ?? null;\n  const max = useWidgetModelValue<IpyDate>(modelId, \"max\") ?? null;\n  const description = useWidgetModelValue<string>(modelId, \"description\");\n  const disabled = useWidgetModelValue<boolean>(modelId, \"disabled\") ?? false;\n\n  const handleChange = useCallback(\n    (e: React.ChangeEvent<HTMLInputElement>) => {\n      const newValue = e.target.value;\n      if (newValue) {\n        const [year, month, day] = newValue.split(\"-\").map(Number);\n        sendUpdate(modelId, { value: { year, month: month - 1, date: day } });\n      } else {\n        sendUpdate(modelId, { value: null });\n      }\n    },\n    [modelId, sendUpdate],\n  );\n\n  return (\n    <div\n      className={cn(\"flex items-center gap-3\", className)}\n      data-widget-id={modelId}\n      data-widget-type=\"DatePicker\"\n    >\n      {description && <Label className=\"shrink-0 text-sm\">{description}</Label>}\n      <Input\n        type=\"date\"\n        value={toDateString(value)}\n        min={toDateString(min)}\n        max={toDateString(max)}\n        disabled={disabled}\n        onChange={handleChange}\n        className=\"w-40\"\n      />\n    </div>\n  );\n}\n\nexport default DatePickerWidget;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/date-picker-widget.tsx"
    },
    {
      "path": "registry/widgets/controls/time-picker-widget.tsx",
      "content": "\"use client\";\n\n/**\n * TimePicker widget - renders a time input field.\n *\n * Maps to ipywidgets TimePickerModel.\n */\n\nimport { useCallback } from \"react\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { cn } from \"@/lib/utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport {\n  useWidgetModelValue,\n  useWidgetStoreRequired,\n} from \"../widget-store-context\";\n\n// Convert time object or string to HH:MM format for input\nfunction toTimeString(\n  value: { hours: number; minutes: number; seconds?: number } | string | null,\n): string {\n  if (!value) return \"\";\n  if (typeof value === \"string\") return value;\n  const hours = String(value.hours).padStart(2, \"0\");\n  const minutes = String(value.minutes).padStart(2, \"0\");\n  return `${hours}:${minutes}`;\n}\n\nexport function TimePickerWidget({ modelId, className }: WidgetComponentProps) {\n  const { sendUpdate } = useWidgetStoreRequired();\n\n  // Subscribe to individual state keys\n  const value =\n    useWidgetModelValue<\n      { hours: number; minutes: number; seconds?: number } | string | null\n    >(modelId, \"value\") ?? null;\n  const min =\n    useWidgetModelValue<{ hours: number; minutes: number } | string | null>(\n      modelId,\n      \"min\",\n    ) ?? null;\n  const max =\n    useWidgetModelValue<{ hours: number; minutes: number } | string | null>(\n      modelId,\n      \"max\",\n    ) ?? null;\n  const description = useWidgetModelValue<string>(modelId, \"description\");\n  const disabled = useWidgetModelValue<boolean>(modelId, \"disabled\") ?? false;\n\n  const handleChange = useCallback(\n    (e: React.ChangeEvent<HTMLInputElement>) => {\n      const newValue = e.target.value;\n      if (newValue) {\n        const [hours, minutes] = newValue.split(\":\").map(Number);\n        sendUpdate(modelId, {\n          value: { hours, minutes, seconds: 0, milliseconds: 0 },\n        });\n      } else {\n        sendUpdate(modelId, { value: null });\n      }\n    },\n    [modelId, sendUpdate],\n  );\n\n  return (\n    <div\n      className={cn(\"flex items-center gap-3\", className)}\n      data-widget-id={modelId}\n      data-widget-type=\"TimePicker\"\n    >\n      {description && <Label className=\"shrink-0 text-sm\">{description}</Label>}\n      <Input\n        type=\"time\"\n        value={toTimeString(value)}\n        min={toTimeString(min)}\n        max={toTimeString(max)}\n        disabled={disabled}\n        onChange={handleChange}\n        className=\"w-32\"\n      />\n    </div>\n  );\n}\n\nexport default TimePickerWidget;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/time-picker-widget.tsx"
    },
    {
      "path": "registry/widgets/controls/float-log-slider.tsx",
      "content": "/**\n * FloatLogSlider widget - renders a logarithmic scale slider.\n *\n * Maps to ipywidgets FloatLogSliderModel.\n */\n\nimport { Label } from \"@/components/ui/label\";\nimport { Slider } from \"@/components/ui/slider\";\nimport { cn } from \"@/lib/utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport {\n  useWidgetModelValue,\n  useWidgetStoreRequired,\n} from \"../widget-store-context\";\n\nexport function FloatLogSlider({ modelId, className }: WidgetComponentProps) {\n  const { sendUpdate } = useWidgetStoreRequired();\n\n  // Subscribe to individual state keys\n  const value = useWidgetModelValue<number>(modelId, \"value\") ?? 1;\n  const base = useWidgetModelValue<number>(modelId, \"base\") ?? 10;\n  const min = useWidgetModelValue<number>(modelId, \"min\") ?? 0; // exponent min\n  const max = useWidgetModelValue<number>(modelId, \"max\") ?? 4; // exponent max\n  const step = useWidgetModelValue<number>(modelId, \"step\") ?? 0.1;\n  const description = useWidgetModelValue<string>(modelId, \"description\");\n  const disabled = useWidgetModelValue<boolean>(modelId, \"disabled\") ?? false;\n  const orientation =\n    useWidgetModelValue<\"horizontal\" | \"vertical\">(modelId, \"orientation\") ??\n    \"horizontal\";\n  const readout = useWidgetModelValue<boolean>(modelId, \"readout\") ?? true;\n  const readoutFormat =\n    useWidgetModelValue<string>(modelId, \"readout_format\") ?? \".3g\";\n\n  // Convert value to log scale position (exponent)\n  const valueToExponent = (v: number): number => {\n    if (v <= 0) return min;\n    return Math.log(v) / Math.log(base);\n  };\n\n  // Convert exponent to actual value\n  const exponentToValue = (exp: number): number => {\n    return base ** exp;\n  };\n\n  const currentExponent = valueToExponent(value);\n\n  const handleChange = (newValue: number[]) => {\n    const newExponent = Math.min(max, Math.max(min, newValue[0]));\n    const newActualValue = exponentToValue(newExponent);\n    sendUpdate(modelId, { value: newActualValue });\n  };\n\n  // Format value for display\n  const formatValue = (v: number): string => {\n    // Parse Python-style format spec (e.g., \".3g\")\n    const matchG = readoutFormat.match(/\\.(\\d+)g/);\n    if (matchG) {\n      return v.toPrecision(parseInt(matchG[1], 10));\n    }\n    const matchF = readoutFormat.match(/\\.(\\d+)f/);\n    if (matchF) {\n      return v.toFixed(parseInt(matchF[1], 10));\n    }\n    const matchE = readoutFormat.match(/\\.(\\d+)e/);\n    if (matchE) {\n      return v.toExponential(parseInt(matchE[1], 10));\n    }\n    return String(v);\n  };\n\n  const isVertical = orientation === \"vertical\";\n\n  return (\n    <div\n      className={cn(\n        \"flex gap-3\",\n        isVertical ? \"flex-col items-center\" : \"items-center\",\n        className,\n      )}\n      data-widget-id={modelId}\n      data-widget-type=\"FloatLogSlider\"\n    >\n      {description && <Label className=\"shrink-0 text-sm\">{description}</Label>}\n      <Slider\n        value={[currentExponent]}\n        min={min}\n        max={max}\n        step={step}\n        disabled={disabled}\n        orientation={orientation}\n        onValueChange={handleChange}\n        className={isVertical ? \"h-32\" : \"min-w-24 flex-1\"}\n      />\n      {readout && (\n        <span className=\"w-20 text-right text-sm tabular-nums text-muted-foreground\">\n          {formatValue(value)}\n        </span>\n      )}\n    </div>\n  );\n}\n\nexport default FloatLogSlider;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/float-log-slider.tsx"
    },
    {
      "path": "registry/widgets/controls/play-widget.tsx",
      "content": "\"use client\";\n\n/**\n * Play widget - animation control with play/pause/step.\n *\n * Maps to ipywidgets PlayModel.\n */\n\nimport {\n  PauseIcon,\n  PlayIcon,\n  SkipBackIcon,\n  SkipForwardIcon,\n} from \"lucide-react\";\nimport { useCallback, useEffect, useRef } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Label } from \"@/components/ui/label\";\nimport { cn } from \"@/lib/utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport {\n  useWidgetModelValue,\n  useWidgetStoreRequired,\n} from \"../widget-store-context\";\n\nexport function PlayWidget({ modelId, className }: WidgetComponentProps) {\n  // sendUpdate is now stable (useCommRouter uses refs internally)\n  const { sendUpdate } = useWidgetStoreRequired();\n  const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);\n\n  // Subscribe to individual state keys\n  const value = useWidgetModelValue<number>(modelId, \"value\") ?? 0;\n  const min = useWidgetModelValue<number>(modelId, \"min\") ?? 0;\n  const max = useWidgetModelValue<number>(modelId, \"max\") ?? 100;\n  const step = useWidgetModelValue<number>(modelId, \"step\") ?? 1;\n  const interval = useWidgetModelValue<number>(modelId, \"interval\") ?? 100; // ms\n  const playing = useWidgetModelValue<boolean>(modelId, \"_playing\") ?? false;\n  const repeat = useWidgetModelValue<boolean>(modelId, \"repeat\") ?? false;\n  const description = useWidgetModelValue<string>(modelId, \"description\");\n  const disabled = useWidgetModelValue<boolean>(modelId, \"disabled\") ?? false;\n\n  // Ref for animation state values - these change frequently and shouldn't restart the interval.\n  // sendUpdate is stable so it doesn't need a ref.\n  const valuesRef = useRef({ value, min, max, step, repeat });\n\n  // Keep values ref up-to-date without triggering the main effect\n  useEffect(() => {\n    valuesRef.current = { value, min, max, step, repeat };\n  });\n\n  // Handle animation interval - only restart when play state or interval timing changes\n  useEffect(() => {\n    if (playing && !disabled) {\n      intervalRef.current = setInterval(() => {\n        const { value, min, max, step, repeat } = valuesRef.current;\n        const nextValue =\n          value + step > max ? (repeat ? min : max) : value + step;\n        const shouldStop = value + step > max && !repeat;\n        sendUpdate(modelId, {\n          value: nextValue,\n          _playing: !shouldStop,\n        });\n      }, interval);\n    } else {\n      if (intervalRef.current) {\n        clearInterval(intervalRef.current);\n        intervalRef.current = null;\n      }\n    }\n\n    return () => {\n      if (intervalRef.current) {\n        clearInterval(intervalRef.current);\n      }\n    };\n  }, [playing, disabled, interval, modelId, sendUpdate]);\n\n  const handlePlayPause = useCallback(() => {\n    sendUpdate(modelId, { _playing: !playing });\n  }, [modelId, playing, sendUpdate]);\n\n  const handleStepBack = useCallback(() => {\n    const { min, step, value } = valuesRef.current;\n    const newValue = Math.max(min, value - step);\n    sendUpdate(modelId, { value: newValue, _playing: false });\n  }, [modelId, sendUpdate]);\n\n  const handleStepForward = useCallback(() => {\n    const { max, step, value } = valuesRef.current;\n    const newValue = Math.min(max, value + step);\n    sendUpdate(modelId, { value: newValue, _playing: false });\n  }, [modelId, sendUpdate]);\n\n  return (\n    <div\n      className={cn(\"flex items-center gap-2\", className)}\n      data-widget-id={modelId}\n      data-widget-type=\"Play\"\n    >\n      {description && <Label className=\"shrink-0 text-sm\">{description}</Label>}\n      <div className=\"flex items-center gap-1\">\n        <Button\n          variant=\"outline\"\n          size=\"icon\"\n          className=\"size-8\"\n          onClick={handleStepBack}\n          disabled={disabled || value <= min}\n        >\n          <SkipBackIcon className=\"size-4\" />\n        </Button>\n        <Button\n          variant=\"outline\"\n          size=\"icon\"\n          className=\"size-8\"\n          onClick={handlePlayPause}\n          disabled={disabled}\n        >\n          {playing ? (\n            <PauseIcon className=\"size-4\" />\n          ) : (\n            <PlayIcon className=\"size-4\" />\n          )}\n        </Button>\n        <Button\n          variant=\"outline\"\n          size=\"icon\"\n          className=\"size-8\"\n          onClick={handleStepForward}\n          disabled={disabled || value >= max}\n        >\n          <SkipForwardIcon className=\"size-4\" />\n        </Button>\n      </div>\n      <span className=\"w-12 text-right text-sm tabular-nums text-muted-foreground\">\n        {value}\n      </span>\n    </div>\n  );\n}\n\nexport default PlayWidget;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/play-widget.tsx"
    },
    {
      "path": "registry/widgets/controls/combobox-widget.tsx",
      "content": "\"use client\";\n\n/**\n * Combobox widget - text input with autocomplete suggestions.\n *\n * Maps to ipywidgets ComboboxModel.\n *\n * Uses native HTML <input> with <datalist> instead of Radix UI Popover\n * because Radix uses Portal which doesn't work in sandboxed iframes\n * (no allow-same-origin). See: https://github.com/runtimed/runt/issues/62\n */\n\nimport { useCallback, useEffect, useId, useState } from \"react\";\nimport { Label } from \"@/components/ui/label\";\nimport { cn } from \"@/lib/utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport {\n  useWidgetModelValue,\n  useWidgetStoreRequired,\n} from \"../widget-store-context\";\n\nexport function ComboboxWidget({ modelId, className }: WidgetComponentProps) {\n  const { sendUpdate, sendCustom } = useWidgetStoreRequired();\n  const datalistId = useId();\n\n  // Subscribe to individual state keys\n  const value = useWidgetModelValue<string>(modelId, \"value\") ?? \"\";\n  const options = useWidgetModelValue<string[]>(modelId, \"options\") ?? [];\n  const placeholder =\n    useWidgetModelValue<string>(modelId, \"placeholder\") ?? \"Select or type...\";\n  const description = useWidgetModelValue<string>(modelId, \"description\");\n  const disabled = useWidgetModelValue<boolean>(modelId, \"disabled\") ?? false;\n  const ensureOption =\n    useWidgetModelValue<boolean>(modelId, \"ensure_option\") ?? false;\n  const continuousUpdate =\n    useWidgetModelValue<boolean>(modelId, \"continuous_update\") ?? true;\n\n  const [inputValue, setInputValue] = useState(value);\n\n  // Sync input value when value changes from kernel\n  useEffect(() => {\n    setInputValue(value);\n  }, [value]);\n\n  const handleChange = useCallback(\n    (event: React.ChangeEvent<HTMLInputElement>) => {\n      const newValue = event.target.value;\n      setInputValue(newValue);\n      if (continuousUpdate) {\n        // Only update if not enforcing option or value is in options\n        if (!ensureOption || options.includes(newValue)) {\n          sendUpdate(modelId, { value: newValue });\n        }\n      }\n    },\n    [modelId, continuousUpdate, ensureOption, options, sendUpdate],\n  );\n\n  const handleBlur = useCallback(() => {\n    if (!continuousUpdate) {\n      if (!ensureOption || options.includes(inputValue)) {\n        sendUpdate(modelId, { value: inputValue });\n      } else {\n        // Reset to last valid value\n        setInputValue(value);\n      }\n    }\n    sendCustom(modelId, { event: \"submit\" });\n  }, [\n    modelId,\n    continuousUpdate,\n    ensureOption,\n    options,\n    inputValue,\n    value,\n    sendUpdate,\n    sendCustom,\n  ]);\n\n  return (\n    <div\n      className={cn(\"flex items-center gap-3\", className)}\n      data-widget-id={modelId}\n      data-widget-type=\"Combobox\"\n    >\n      {description && <Label className=\"shrink-0 text-sm\">{description}</Label>}\n      <input\n        type=\"text\"\n        list={datalistId}\n        value={inputValue}\n        onChange={handleChange}\n        onBlur={handleBlur}\n        placeholder={placeholder}\n        disabled={disabled}\n        className={cn(\n          // Base styles matching shadcn input\n          \"h-9 w-48 rounded-md border border-input px-3 py-2 text-sm\",\n          \"bg-transparent shadow-xs transition-[color,box-shadow] outline-none\",\n          // Dark mode background\n          \"dark:bg-input/30 dark:hover:bg-input/50\",\n          // Placeholder\n          \"placeholder:text-muted-foreground\",\n          // Focus styles\n          \"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50\",\n          // Disabled state\n          \"disabled:cursor-not-allowed disabled:opacity-50\",\n        )}\n      />\n      <datalist id={datalistId}>\n        {options.map((option, idx) => (\n          <option key={idx} value={option} />\n        ))}\n      </datalist>\n    </div>\n  );\n}\n\nexport default ComboboxWidget;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/combobox-widget.tsx"
    },
    {
      "path": "registry/widgets/controls/selection-slider-widget.tsx",
      "content": "/**\n * SelectionSlider widget - slider that selects from discrete options.\n *\n * Maps to ipywidgets SelectionSliderModel.\n */\n\nimport { Label } from \"@/components/ui/label\";\nimport { Slider } from \"@/components/ui/slider\";\nimport { cn } from \"@/lib/utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport {\n  useWidgetModelValue,\n  useWidgetStoreRequired,\n} from \"../widget-store-context\";\n\nexport function SelectionSliderWidget({\n  modelId,\n  className,\n}: WidgetComponentProps) {\n  const { sendUpdate } = useWidgetStoreRequired();\n\n  // Subscribe to individual state keys\n  const options =\n    useWidgetModelValue<string[]>(modelId, \"_options_labels\") ?? [];\n  const index = useWidgetModelValue<number>(modelId, \"index\") ?? 0;\n  const description = useWidgetModelValue<string>(modelId, \"description\");\n  const disabled = useWidgetModelValue<boolean>(modelId, \"disabled\") ?? false;\n  const orientation =\n    useWidgetModelValue<\"horizontal\" | \"vertical\">(modelId, \"orientation\") ??\n    \"horizontal\";\n  const readout = useWidgetModelValue<boolean>(modelId, \"readout\") ?? true;\n  const continuousUpdate =\n    useWidgetModelValue<boolean>(modelId, \"continuous_update\") ?? true;\n\n  const handleChange = (newValue: number[]) => {\n    const newIndex = Math.round(newValue[0]);\n    const clampedIndex = Math.min(options.length - 1, Math.max(0, newIndex));\n    if (continuousUpdate || clampedIndex !== index) {\n      sendUpdate(modelId, { index: clampedIndex });\n    }\n  };\n\n  const isVertical = orientation === \"vertical\";\n  const currentLabel = options[index] ?? \"\";\n\n  return (\n    <div\n      className={cn(\n        \"flex gap-3\",\n        isVertical ? \"flex-col items-center\" : \"items-center\",\n        className,\n      )}\n      data-widget-id={modelId}\n      data-widget-type=\"SelectionSlider\"\n    >\n      {description && <Label className=\"shrink-0 text-sm\">{description}</Label>}\n      <Slider\n        value={[index]}\n        min={0}\n        max={Math.max(0, options.length - 1)}\n        step={1}\n        disabled={disabled || options.length === 0}\n        orientation={orientation}\n        onValueChange={handleChange}\n        className={isVertical ? \"h-32\" : \"min-w-24 flex-1\"}\n      />\n      {readout && (\n        <span className=\"min-w-16 text-sm text-muted-foreground\">\n          {currentLabel}\n        </span>\n      )}\n    </div>\n  );\n}\n\nexport default SelectionSliderWidget;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/selection-slider-widget.tsx"
    },
    {
      "path": "registry/widgets/controls/selection-range-slider-widget.tsx",
      "content": "/**\n * SelectionRangeSlider widget - range slider that selects from discrete options.\n *\n * Maps to ipywidgets SelectionRangeSliderModel.\n */\n\nimport { Label } from \"@/components/ui/label\";\nimport { Slider } from \"@/components/ui/slider\";\nimport { cn } from \"@/lib/utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport {\n  useWidgetModelValue,\n  useWidgetStoreRequired,\n} from \"../widget-store-context\";\n\nexport function SelectionRangeSliderWidget({\n  modelId,\n  className,\n}: WidgetComponentProps) {\n  const { sendUpdate } = useWidgetStoreRequired();\n\n  // Subscribe to individual state keys\n  const options =\n    useWidgetModelValue<string[]>(modelId, \"_options_labels\") ?? [];\n  const index = useWidgetModelValue<number[]>(modelId, \"index\") ?? [0, 0];\n  const description = useWidgetModelValue<string>(modelId, \"description\");\n  const disabled = useWidgetModelValue<boolean>(modelId, \"disabled\") ?? false;\n  const orientation =\n    useWidgetModelValue<\"horizontal\" | \"vertical\">(modelId, \"orientation\") ??\n    \"horizontal\";\n  const readout = useWidgetModelValue<boolean>(modelId, \"readout\") ?? true;\n  const continuousUpdate =\n    useWidgetModelValue<boolean>(modelId, \"continuous_update\") ?? true;\n\n  const [lowIndex, highIndex] = index;\n\n  const handleChange = (newValue: number[]) => {\n    const newLow = Math.round(newValue[0]);\n    const newHigh = Math.round(newValue[1]);\n    const clampedLow = Math.min(options.length - 1, Math.max(0, newLow));\n    const clampedHigh = Math.min(options.length - 1, Math.max(0, newHigh));\n\n    if (\n      continuousUpdate ||\n      clampedLow !== lowIndex ||\n      clampedHigh !== highIndex\n    ) {\n      sendUpdate(modelId, { index: [clampedLow, clampedHigh] });\n    }\n  };\n\n  const isVertical = orientation === \"vertical\";\n  const lowLabel = options[lowIndex] ?? \"\";\n  const highLabel = options[highIndex] ?? \"\";\n\n  return (\n    <div\n      className={cn(\n        \"flex gap-3\",\n        isVertical ? \"flex-col items-center\" : \"items-center\",\n        className,\n      )}\n      data-widget-id={modelId}\n      data-widget-type=\"SelectionRangeSlider\"\n    >\n      {description && <Label className=\"shrink-0 text-sm\">{description}</Label>}\n      <Slider\n        value={[lowIndex, highIndex]}\n        min={0}\n        max={Math.max(0, options.length - 1)}\n        step={1}\n        disabled={disabled || options.length === 0}\n        orientation={orientation}\n        onValueChange={handleChange}\n        className={isVertical ? \"h-32\" : \"min-w-24 flex-1\"}\n      />\n      {readout && (\n        <span className=\"min-w-24 text-sm text-muted-foreground\">\n          {lowLabel} - {highLabel}\n        </span>\n      )}\n    </div>\n  );\n}\n\nexport default SelectionRangeSliderWidget;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/selection-range-slider-widget.tsx"
    },
    {
      "path": "registry/widgets/controls/file-upload-widget.tsx",
      "content": "\"use client\";\n\n/**\n * FileUpload widget - file upload button.\n *\n * Maps to ipywidgets FileUploadModel.\n */\n\nimport { UploadIcon } from \"lucide-react\";\nimport { useCallback, useRef } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Label } from \"@/components/ui/label\";\nimport { cn } from \"@/lib/utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport {\n  useWidgetModelValue,\n  useWidgetStoreRequired,\n} from \"../widget-store-context\";\nimport { getButtonStyle } from \"./button-style-utils\";\n\ninterface FileData {\n  name: string;\n  type: string;\n  size: number;\n  content: string; // base64\n  last_modified: number;\n}\n\nexport function FileUploadWidget({ modelId, className }: WidgetComponentProps) {\n  const { sendUpdate } = useWidgetStoreRequired();\n  const inputRef = useRef<HTMLInputElement>(null);\n\n  // Subscribe to individual state keys\n  const value = useWidgetModelValue<FileData[]>(modelId, \"value\") ?? [];\n  const accept = useWidgetModelValue<string>(modelId, \"accept\") ?? \"\";\n  const multiple = useWidgetModelValue<boolean>(modelId, \"multiple\") ?? false;\n  const description =\n    useWidgetModelValue<string>(modelId, \"description\") ?? \"Upload\";\n  const disabled = useWidgetModelValue<boolean>(modelId, \"disabled\") ?? false;\n  const buttonStyle =\n    useWidgetModelValue<string>(modelId, \"button_style\") ?? \"\";\n  const icon = useWidgetModelValue<string>(modelId, \"icon\") ?? \"upload\";\n\n  const { variant, className: styleClassName } = getButtonStyle(buttonStyle);\n\n  const handleClick = useCallback(() => {\n    inputRef.current?.click();\n  }, []);\n\n  const handleFileChange = useCallback(\n    async (e: React.ChangeEvent<HTMLInputElement>) => {\n      const files = e.target.files;\n      if (!files || files.length === 0) return;\n\n      const fileDataArray: FileData[] = [];\n\n      for (const file of Array.from(files)) {\n        // Read file as base64\n        const content = await new Promise<string>((resolve, reject) => {\n          const reader = new FileReader();\n          reader.onload = () => {\n            const result = reader.result as string;\n            // Remove data URL prefix to get just base64\n            const base64 = result.split(\",\")[1] || \"\";\n            resolve(base64);\n          };\n          reader.onerror = reject;\n          reader.readAsDataURL(file);\n        });\n\n        fileDataArray.push({\n          name: file.name,\n          type: file.type,\n          size: file.size,\n          content,\n          last_modified: file.lastModified,\n        });\n      }\n\n      sendUpdate(modelId, { value: fileDataArray });\n\n      // Reset input so same file can be selected again\n      if (inputRef.current) {\n        inputRef.current.value = \"\";\n      }\n    },\n    [modelId, sendUpdate],\n  );\n\n  const fileCount = value.length;\n  const buttonText =\n    fileCount > 0\n      ? `${fileCount} file${fileCount > 1 ? \"s\" : \"\"} selected`\n      : description;\n\n  return (\n    <div\n      className={cn(\"flex items-center gap-3\", className)}\n      data-widget-id={modelId}\n      data-widget-type=\"FileUpload\"\n    >\n      <input\n        ref={inputRef}\n        type=\"file\"\n        accept={accept}\n        multiple={multiple}\n        onChange={handleFileChange}\n        disabled={disabled}\n        className=\"hidden\"\n      />\n      <Button\n        variant={variant}\n        onClick={handleClick}\n        disabled={disabled}\n        className={cn(styleClassName, \"gap-2\")}\n      >\n        {icon === \"upload\" && <UploadIcon className=\"size-4\" />}\n        {buttonText}\n      </Button>\n      {fileCount > 0 && (\n        <Label className=\"text-sm text-muted-foreground\">\n          {value.map((f) => f.name).join(\", \")}\n        </Label>\n      )}\n    </div>\n  );\n}\n\nexport default FileUploadWidget;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/file-upload-widget.tsx"
    },
    {
      "path": "registry/widgets/controls/tags-input-widget.tsx",
      "content": "\"use client\";\n\n/**\n * TagsInput widget - multi-value string tag input.\n *\n * Maps to ipywidgets TagsInputModel.\n */\n\nimport { XIcon } from \"lucide-react\";\nimport { useCallback, useState } from \"react\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { cn } from \"@/lib/utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport {\n  useWidgetModelValue,\n  useWidgetStoreRequired,\n} from \"../widget-store-context\";\n\nexport function TagsInputWidget({ modelId, className }: WidgetComponentProps) {\n  const { sendUpdate } = useWidgetStoreRequired();\n\n  // Subscribe to individual state keys\n  const value = useWidgetModelValue<string[]>(modelId, \"value\") ?? [];\n  const allowedTags = useWidgetModelValue<string[]>(modelId, \"allowed_tags\");\n  const allowDuplicates =\n    useWidgetModelValue<boolean>(modelId, \"allow_duplicates\") ?? true;\n  const placeholder =\n    useWidgetModelValue<string>(modelId, \"placeholder\") ?? \"Add tag...\";\n  const description = useWidgetModelValue<string>(modelId, \"description\");\n  const disabled = useWidgetModelValue<boolean>(modelId, \"disabled\") ?? false;\n\n  const [inputValue, setInputValue] = useState(\"\");\n\n  const handleKeyDown = useCallback(\n    (e: React.KeyboardEvent<HTMLInputElement>) => {\n      if (e.key === \"Enter\" && inputValue.trim()) {\n        e.preventDefault();\n        const newTag = inputValue.trim();\n\n        // Check if tag is allowed\n        if (\n          allowedTags &&\n          allowedTags.length > 0 &&\n          !allowedTags.includes(newTag)\n        ) {\n          return;\n        }\n\n        // Check for duplicates\n        if (!allowDuplicates && value.includes(newTag)) {\n          return;\n        }\n\n        sendUpdate(modelId, { value: [...value, newTag] });\n        setInputValue(\"\");\n      } else if (e.key === \"Backspace\" && !inputValue && value.length > 0) {\n        // Remove last tag on backspace when input is empty\n        sendUpdate(modelId, { value: value.slice(0, -1) });\n      }\n    },\n    [modelId, inputValue, value, allowedTags, allowDuplicates, sendUpdate],\n  );\n\n  const handleRemoveTag = useCallback(\n    (indexToRemove: number) => {\n      sendUpdate(modelId, {\n        value: value.filter((_, idx) => idx !== indexToRemove),\n      });\n    },\n    [modelId, value, sendUpdate],\n  );\n\n  return (\n    <div\n      className={cn(\"flex items-start gap-3\", className)}\n      data-widget-id={modelId}\n      data-widget-type=\"TagsInput\"\n    >\n      {description && (\n        <Label className=\"shrink-0 pt-2 text-sm\">{description}</Label>\n      )}\n      <div\n        className={cn(\n          \"flex min-h-10 flex-wrap items-center gap-1.5 rounded-md border border-input bg-background px-3 py-2\",\n          disabled && \"cursor-not-allowed opacity-50\",\n        )}\n      >\n        {value.map((tag, idx) => (\n          <Badge\n            key={`${tag}-${idx}`}\n            variant=\"secondary\"\n            className=\"gap-1 pr-1\"\n          >\n            {tag}\n            {!disabled && (\n              <button\n                type=\"button\"\n                onClick={() => handleRemoveTag(idx)}\n                className=\"rounded-full p-0.5 hover:bg-muted\"\n              >\n                <XIcon className=\"size-3\" />\n              </button>\n            )}\n          </Badge>\n        ))}\n        <Input\n          type=\"text\"\n          value={inputValue}\n          onChange={(e) => setInputValue(e.target.value)}\n          onKeyDown={handleKeyDown}\n          placeholder={value.length === 0 ? placeholder : \"\"}\n          disabled={disabled}\n          className=\"h-6 min-w-20 flex-1 border-0 p-0 shadow-none focus-visible:ring-0\"\n        />\n      </div>\n    </div>\n  );\n}\n\nexport default TagsInputWidget;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/tags-input-widget.tsx"
    },
    {
      "path": "registry/widgets/controls/colors-input-widget.tsx",
      "content": "\"use client\";\n\n/**\n * ColorsInput widget - multi-value color input.\n *\n * Maps to ipywidgets ColorsInputModel.\n */\n\nimport { PlusIcon, XIcon } from \"lucide-react\";\nimport { useCallback, useRef, useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Label } from \"@/components/ui/label\";\nimport { cn } from \"@/lib/utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport {\n  useWidgetModelValue,\n  useWidgetStoreRequired,\n} from \"../widget-store-context\";\n\nexport function ColorsInputWidget({\n  modelId,\n  className,\n}: WidgetComponentProps) {\n  const { sendUpdate } = useWidgetStoreRequired();\n  const colorInputRef = useRef<HTMLInputElement>(null);\n\n  // Subscribe to individual state keys\n  const value = useWidgetModelValue<string[]>(modelId, \"value\") ?? [];\n  const allowedTags = useWidgetModelValue<string[]>(modelId, \"allowed_tags\");\n  const allowDuplicates =\n    useWidgetModelValue<boolean>(modelId, \"allow_duplicates\") ?? true;\n  const description = useWidgetModelValue<string>(modelId, \"description\");\n  const disabled = useWidgetModelValue<boolean>(modelId, \"disabled\") ?? false;\n\n  const [editingIndex, setEditingIndex] = useState<number | null>(null);\n\n  const handleAddColor = useCallback(() => {\n    colorInputRef.current?.click();\n  }, []);\n\n  const handleColorChange = useCallback(\n    (e: React.ChangeEvent<HTMLInputElement>) => {\n      const newColor = e.target.value;\n\n      // Check if color is allowed\n      if (\n        allowedTags &&\n        allowedTags.length > 0 &&\n        !allowedTags.includes(newColor)\n      ) {\n        return;\n      }\n\n      // Check for duplicates\n      if (!allowDuplicates && value.includes(newColor)) {\n        return;\n      }\n\n      if (editingIndex !== null) {\n        // Update existing color\n        const newValue = [...value];\n        newValue[editingIndex] = newColor;\n        sendUpdate(modelId, { value: newValue });\n        setEditingIndex(null);\n      } else {\n        // Add new color\n        sendUpdate(modelId, { value: [...value, newColor] });\n      }\n    },\n    [modelId, value, allowedTags, allowDuplicates, editingIndex, sendUpdate],\n  );\n\n  const handleRemoveColor = useCallback(\n    (indexToRemove: number) => {\n      sendUpdate(modelId, {\n        value: value.filter((_, idx) => idx !== indexToRemove),\n      });\n    },\n    [modelId, value, sendUpdate],\n  );\n\n  const handleEditColor = useCallback((idx: number) => {\n    setEditingIndex(idx);\n    // Trigger color picker with slight delay to allow state update\n    setTimeout(() => {\n      colorInputRef.current?.click();\n    }, 0);\n  }, []);\n\n  return (\n    <div\n      className={cn(\"flex items-start gap-3\", className)}\n      data-widget-id={modelId}\n      data-widget-type=\"ColorsInput\"\n    >\n      {description && (\n        <Label className=\"shrink-0 pt-2 text-sm\">{description}</Label>\n      )}\n      <div\n        className={cn(\n          \"flex flex-wrap items-center gap-2\",\n          disabled && \"cursor-not-allowed opacity-50\",\n        )}\n      >\n        {value.map((color, idx) => (\n          <div\n            key={`${color}-${idx}`}\n            className=\"group relative flex items-center\"\n          >\n            <button\n              type=\"button\"\n              onClick={() => handleEditColor(idx)}\n              disabled={disabled}\n              className=\"size-8 rounded-md border border-input shadow-sm transition-transform hover:scale-105\"\n              style={{ backgroundColor: color }}\n              title={color}\n            />\n            {!disabled && (\n              <button\n                type=\"button\"\n                onClick={() => handleRemoveColor(idx)}\n                className=\"absolute -right-1 -top-1 rounded-full bg-background p-0.5 opacity-0 shadow-sm transition-opacity group-hover:opacity-100\"\n              >\n                <XIcon className=\"size-3\" />\n              </button>\n            )}\n          </div>\n        ))}\n        <input\n          ref={colorInputRef}\n          type=\"color\"\n          value={editingIndex !== null ? value[editingIndex] : \"#000000\"}\n          onChange={handleColorChange}\n          disabled={disabled}\n          className=\"sr-only\"\n        />\n        <Button\n          variant=\"outline\"\n          size=\"icon\"\n          className=\"size-8\"\n          onClick={handleAddColor}\n          disabled={disabled}\n        >\n          <PlusIcon className=\"size-4\" />\n        </Button>\n      </div>\n    </div>\n  );\n}\n\nexport default ColorsInputWidget;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/colors-input-widget.tsx"
    },
    {
      "path": "registry/widgets/controls/controller-widget.tsx",
      "content": "\"use client\";\n\n/**\n * Controller widget - gamepad input via Web Gamepad API.\n *\n * Maps to ipywidgets ControllerModel. Polls connected gamepads\n * and updates button/axis child widgets with current state.\n */\n\nimport { GamepadIcon } from \"lucide-react\";\nimport { useCallback, useEffect, useRef } from \"react\";\nimport { Label } from \"@/components/ui/label\";\nimport { cn } from \"@/lib/utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport {\n  parseModelRef,\n  useWidgetModelValue,\n  useWidgetStoreRequired,\n} from \"../widget-store-context\";\nimport { WidgetView } from \"../widget-view\";\n\nexport function ControllerWidget({ modelId, className }: WidgetComponentProps) {\n  // sendUpdate is now stable (useCommRouter uses refs internally)\n  const { sendUpdate } = useWidgetStoreRequired();\n  const animationRef = useRef<number | null>(null);\n  const lastStateRef = useRef<{\n    buttons: { pressed: boolean; value: number }[];\n    axes: number[];\n  } | null>(null);\n\n  // Subscribe to widget state\n  const index = useWidgetModelValue<number>(modelId, \"index\") ?? 0;\n  const connected = useWidgetModelValue<boolean>(modelId, \"connected\") ?? false;\n  const name = useWidgetModelValue<string>(modelId, \"name\") ?? \"\";\n  const mapping = useWidgetModelValue<string>(modelId, \"mapping\") ?? \"\";\n  const buttons = useWidgetModelValue<string[]>(modelId, \"buttons\") ?? [];\n  const axes = useWidgetModelValue<string[]>(modelId, \"axes\") ?? [];\n\n  // Ref for polling state values - these change during polling and shouldn't restart the rAF loop.\n  // sendUpdate is stable so it doesn't need a ref.\n  const stateRef = useRef({ connected, buttons, axes });\n\n  // Keep state ref up-to-date without triggering the polling effect\n  useEffect(() => {\n    stateRef.current = { connected, buttons, axes };\n  });\n\n  // Poll gamepad state - only depends on index, modelId, sendUpdate to avoid loop restarts\n  const pollGamepad = useCallback(() => {\n    const gamepads = navigator.getGamepads();\n    const gamepad = gamepads[index];\n    const { connected, buttons, axes } = stateRef.current;\n\n    if (gamepad) {\n      // Check if we need to update connection state\n      if (!connected) {\n        sendUpdate(modelId, {\n          connected: true,\n          name: gamepad.id,\n          mapping: gamepad.mapping,\n          timestamp: gamepad.timestamp,\n        });\n      }\n\n      const lastState = lastStateRef.current;\n      let hasChanges = false;\n\n      // Update button states\n      gamepad.buttons.forEach((button, i) => {\n        const buttonRef = buttons[i];\n        if (!buttonRef) return;\n\n        const buttonId = parseModelRef(buttonRef);\n        if (!buttonId) return;\n\n        const lastButton = lastState?.buttons[i];\n        if (\n          !lastButton ||\n          lastButton.pressed !== button.pressed ||\n          Math.abs(lastButton.value - button.value) > 0.01\n        ) {\n          sendUpdate(buttonId, {\n            pressed: button.pressed,\n            value: button.value,\n          });\n          hasChanges = true;\n        }\n      });\n\n      // Update axis states\n      gamepad.axes.forEach((axisValue, i) => {\n        const axisRef = axes[i];\n        if (!axisRef) return;\n\n        const axisId = parseModelRef(axisRef);\n        if (!axisId) return;\n\n        const lastAxis = lastState?.axes[i];\n        if (lastAxis === undefined || Math.abs(lastAxis - axisValue) > 0.01) {\n          sendUpdate(axisId, { value: axisValue });\n          hasChanges = true;\n        }\n      });\n\n      // Store current state for comparison\n      if (hasChanges || !lastState) {\n        lastStateRef.current = {\n          buttons: gamepad.buttons.map((b) => ({\n            pressed: b.pressed,\n            value: b.value,\n          })),\n          axes: [...gamepad.axes],\n        };\n      }\n\n      // Update timestamp\n      if (hasChanges) {\n        sendUpdate(modelId, { timestamp: gamepad.timestamp });\n      }\n    } else if (connected) {\n      // Gamepad disconnected\n      sendUpdate(modelId, { connected: false });\n      lastStateRef.current = null;\n    }\n\n    animationRef.current = requestAnimationFrame(pollGamepad);\n  }, [index, modelId, sendUpdate]);\n\n  // Start/stop polling based on component lifecycle\n  useEffect(() => {\n    // Start polling\n    animationRef.current = requestAnimationFrame(pollGamepad);\n\n    // Handle gamepad connect/disconnect events\n    const handleConnect = (e: GamepadEvent) => {\n      if (e.gamepad.index === index) {\n        sendUpdate(modelId, {\n          connected: true,\n          name: e.gamepad.id,\n          mapping: e.gamepad.mapping,\n        });\n      }\n    };\n\n    const handleDisconnect = (e: GamepadEvent) => {\n      if (e.gamepad.index === index) {\n        sendUpdate(modelId, { connected: false });\n        lastStateRef.current = null;\n      }\n    };\n\n    window.addEventListener(\"gamepadconnected\", handleConnect);\n    window.addEventListener(\"gamepaddisconnected\", handleDisconnect);\n\n    return () => {\n      if (animationRef.current) {\n        cancelAnimationFrame(animationRef.current);\n      }\n      window.removeEventListener(\"gamepadconnected\", handleConnect);\n      window.removeEventListener(\"gamepaddisconnected\", handleDisconnect);\n    };\n  }, [index, modelId, pollGamepad, sendUpdate]);\n\n  return (\n    <div\n      className={cn(\"flex flex-col gap-3 rounded-lg border p-4\", className)}\n      data-widget-id={modelId}\n      data-widget-type=\"Controller\"\n    >\n      {/* Header */}\n      <div className=\"flex items-center gap-2\">\n        <GamepadIcon\n          className={cn(\n            \"size-5\",\n            connected ? \"text-green-500\" : \"text-muted-foreground\",\n          )}\n        />\n        <Label className=\"font-medium\">{name || `Controller ${index}`}</Label>\n        <span\n          className={cn(\n            \"ml-auto text-xs\",\n            connected ? \"text-green-500\" : \"text-muted-foreground\",\n          )}\n        >\n          {connected ? \"Connected\" : \"Disconnected\"}\n        </span>\n      </div>\n\n      {connected && (\n        <>\n          {/* Buttons */}\n          {buttons.length > 0 && (\n            <div className=\"space-y-1\">\n              <Label className=\"text-xs text-muted-foreground\">Buttons</Label>\n              <div className=\"flex flex-wrap gap-1\">\n                {buttons.map((buttonRef, i) => {\n                  const buttonId = parseModelRef(buttonRef);\n                  return buttonId ? (\n                    <WidgetView key={buttonId} modelId={buttonId} />\n                  ) : (\n                    <div\n                      key={i}\n                      className=\"flex size-8 items-center justify-center rounded-full border bg-muted text-xs\"\n                    >\n                      {i}\n                    </div>\n                  );\n                })}\n              </div>\n            </div>\n          )}\n\n          {/* Axes */}\n          {axes.length > 0 && (\n            <div className=\"space-y-1\">\n              <Label className=\"text-xs text-muted-foreground\">Axes</Label>\n              <div className=\"flex flex-col gap-1\">\n                {axes.map((axisRef, i) => {\n                  const axisId = parseModelRef(axisRef);\n                  return axisId ? (\n                    <div key={axisId} className=\"flex items-center gap-2\">\n                      <span className=\"w-8 text-xs text-muted-foreground\">\n                        {i}:\n                      </span>\n                      <WidgetView modelId={axisId} />\n                    </div>\n                  ) : null;\n                })}\n              </div>\n            </div>\n          )}\n\n          {/* Mapping info */}\n          {mapping && (\n            <div className=\"text-xs text-muted-foreground\">\n              Mapping: {mapping}\n            </div>\n          )}\n        </>\n      )}\n\n      {!connected && (\n        <div className=\"py-4 text-center text-sm text-muted-foreground\">\n          Press a button on a gamepad to connect\n        </div>\n      )}\n    </div>\n  );\n}\n\nexport default ControllerWidget;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/controller-widget.tsx"
    },
    {
      "path": "registry/widgets/controls/controller-button-widget.tsx",
      "content": "/**\n * ControllerButton widget - displays a gamepad button state.\n *\n * Maps to ipywidgets ControllerButtonModel.\n */\n\nimport { cn } from \"@/lib/utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport { useWidgetModelValue } from \"../widget-store-context\";\n\nexport function ControllerButtonWidget({\n  modelId,\n  className,\n}: WidgetComponentProps) {\n  const pressed = useWidgetModelValue<boolean>(modelId, \"pressed\") ?? false;\n  const value = useWidgetModelValue<number>(modelId, \"value\") ?? 0;\n\n  return (\n    <div\n      className={cn(\n        \"flex size-8 items-center justify-center rounded-full border-2 text-xs font-medium transition-colors\",\n        pressed\n          ? \"border-primary bg-primary text-primary-foreground\"\n          : \"border-muted-foreground/30 bg-muted text-muted-foreground\",\n        className,\n      )}\n      data-widget-id={modelId}\n      data-widget-type=\"ControllerButton\"\n      title={`Value: ${value.toFixed(2)}`}\n    >\n      {pressed ? \"●\" : \"○\"}\n    </div>\n  );\n}\n\nexport default ControllerButtonWidget;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/controller-button-widget.tsx"
    },
    {
      "path": "registry/widgets/controls/controller-axis-widget.tsx",
      "content": "/**\n * ControllerAxis widget - displays a gamepad axis value.\n *\n * Maps to ipywidgets ControllerAxisModel.\n */\n\nimport { Progress } from \"@/components/ui/progress\";\nimport { cn } from \"@/lib/utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport { useWidgetModelValue } from \"../widget-store-context\";\n\nexport function ControllerAxisWidget({\n  modelId,\n  className,\n}: WidgetComponentProps) {\n  // Axis value ranges from -1 to 1\n  const value = useWidgetModelValue<number>(modelId, \"value\") ?? 0;\n\n  // Convert -1..1 to 0..100 for progress display\n  const progressValue = ((value + 1) / 2) * 100;\n\n  return (\n    <div\n      className={cn(\"flex items-center gap-2\", className)}\n      data-widget-id={modelId}\n      data-widget-type=\"ControllerAxis\"\n    >\n      <Progress value={progressValue} className=\"h-2 w-16\" />\n      <span className=\"w-12 text-right text-xs tabular-nums text-muted-foreground\">\n        {value.toFixed(2)}\n      </span>\n    </div>\n  );\n}\n\nexport default ControllerAxisWidget;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/controller-axis-widget.tsx"
    },
    {
      "path": "registry/widgets/controls/output-widget.tsx",
      "content": "\"use client\";\n\n/**\n * Output widget - renders captured Jupyter outputs.\n *\n * Maps to ipywidgets OutputModel (@jupyter-widgets/output).\n * Renders an array of Jupyter outputs using the OutputArea component.\n * Media rendering configuration (custom renderers, priority) is\n * inherited from MediaProvider context if present.\n *\n * Note: The Output widget protocol is particularly complex. Rather than\n * simply setting outputs on the model from Python, Jupyter sends outputs\n * as custom messages that must be accumulated client-side. This includes\n * handling clear_output(wait=True) which defers clearing until the next\n * output arrives. We sync rendered state back to the model to keep\n * Python's `out.outputs` in sync with what's displayed.\n */\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { type JupyterOutput, OutputArea } from \"@/registry/cell/OutputArea\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport {\n  useWidgetModelValue,\n  useWidgetStoreRequired,\n} from \"../widget-store-context\";\n\ninterface OutputCustomMessage {\n  method?: unknown;\n  output?: unknown;\n  wait?: unknown;\n}\n\nfunction isJupyterOutput(value: unknown): value is JupyterOutput {\n  if (!value || typeof value !== \"object\") return false;\n  const output = value as Partial<JupyterOutput>;\n  return (\n    output.output_type === \"execute_result\" ||\n    output.output_type === \"display_data\" ||\n    output.output_type === \"stream\" ||\n    output.output_type === \"error\"\n  );\n}\n\nexport function OutputWidget({ modelId, className }: WidgetComponentProps) {\n  const { store, sendUpdate } = useWidgetStoreRequired();\n  const stateOutputs =\n    useWidgetModelValue<JupyterOutput[]>(modelId, \"outputs\") ?? [];\n  const stateOutputsRef = useRef(stateOutputs);\n  const shouldClearOnNextOutputRef = useRef(false);\n  const [renderedOutputs, setRenderedOutputs] =\n    useState<JupyterOutput[]>(stateOutputs);\n  const renderedOutputsRef = useRef(renderedOutputs);\n\n  useEffect(() => {\n    stateOutputsRef.current = stateOutputs;\n    renderedOutputsRef.current = stateOutputs;\n    setRenderedOutputs(stateOutputs);\n  }, [stateOutputs]);\n\n  useEffect(() => {\n    renderedOutputsRef.current = renderedOutputs;\n  }, [renderedOutputs]);\n\n  useEffect(() => {\n    let replayingBufferedMessages = true;\n\n    const unsubscribe = store.subscribeToCustomMessage(modelId, (content) => {\n      const message = content as OutputCustomMessage;\n      const method =\n        typeof message.method === \"string\" ? message.method : undefined;\n\n      // OutputModel may have already synchronized state.outputs.\n      // Skip initial buffered custom messages in that case to avoid duplicates.\n      if (replayingBufferedMessages && stateOutputsRef.current.length > 0) {\n        return;\n      }\n\n      if (method === \"clear_output\") {\n        const wait = Boolean(message.wait);\n        if (wait) {\n          shouldClearOnNextOutputRef.current = true;\n        } else {\n          shouldClearOnNextOutputRef.current = false;\n          renderedOutputsRef.current = [];\n          setRenderedOutputs([]);\n          // Keep Python-side `out.outputs` in sync with displayed output state.\n          sendUpdate(modelId, { outputs: [] });\n        }\n        return;\n      }\n\n      if (method !== \"output\" || !isJupyterOutput(message.output)) {\n        return;\n      }\n      const nextOutput: JupyterOutput = message.output;\n\n      const prev = renderedOutputsRef.current;\n      const next = shouldClearOnNextOutputRef.current\n        ? [nextOutput]\n        : [...prev, nextOutput];\n      shouldClearOnNextOutputRef.current = false;\n      renderedOutputsRef.current = next;\n      setRenderedOutputs(next);\n      // Keep Python-side `out.outputs` in sync with displayed output state.\n      sendUpdate(modelId, { outputs: next });\n    });\n\n    replayingBufferedMessages = false;\n    return unsubscribe;\n  }, [modelId, sendUpdate, store]);\n\n  if (renderedOutputs.length === 0) {\n    return null;\n  }\n\n  // Check if we're already inside an iframe (isolated context).\n  // If so, skip nested isolation since the outer iframe already provides security.\n  // This prevents double-nesting: widget iframe → OutputWidget iframe → content\n  const isInIframe = typeof window !== \"undefined\" && window.parent !== window;\n\n  return (\n    <div\n      className={cn(\"widget-output\", className)}\n      data-widget-id={modelId}\n      data-widget-type=\"Output\"\n    >\n      <OutputArea\n        outputs={renderedOutputs}\n        isolated={isInIframe ? false : \"auto\"}\n      />\n    </div>\n  );\n}\n\nexport default OutputWidget;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/output-widget.tsx"
    },
    {
      "path": "registry/widgets/controls/button-style-utils.ts",
      "content": "/**\n * Shared utility for mapping ipywidgets button_style to shadcn Button\n * variant + Tailwind className overrides.\n *\n * The shadcn Button only has default, destructive, outline, secondary, ghost,\n * link variants — no info/success/warning. We use the \"default\" variant as a\n * base and apply Tailwind color classes via className. tailwind-merge resolves\n * the conflict in favor of our overrides.\n */\n\ntype ButtonStyleVariant = \"default\" | \"destructive\" | \"outline\";\n\ninterface ButtonStyleResult {\n  variant: ButtonStyleVariant;\n  className: string;\n}\n\nconst BUTTON_STYLE_MAP: Record<string, ButtonStyleResult> = {\n  primary: {\n    variant: \"default\",\n    className: \"bg-blue-600 text-white hover:bg-blue-700\",\n  },\n  success: {\n    variant: \"default\",\n    className: \"bg-green-600 text-white hover:bg-green-700\",\n  },\n  info: {\n    variant: \"default\",\n    className: \"bg-sky-600 text-white hover:bg-sky-700\",\n  },\n  warning: {\n    variant: \"default\",\n    className: \"bg-amber-500 text-white hover:bg-amber-600\",\n  },\n  danger: {\n    variant: \"destructive\",\n    className: \"\",\n  },\n  \"\": {\n    variant: \"outline\",\n    className: \"\",\n  },\n};\n\nconst DEFAULT_STYLE: ButtonStyleResult = { variant: \"outline\", className: \"\" };\n\nexport function getButtonStyle(buttonStyle: string): ButtonStyleResult {\n  return BUTTON_STYLE_MAP[buttonStyle] ?? DEFAULT_STYLE;\n}\n",
      "type": "registry:lib",
      "target": "components/widgets/controls/button-style-utils.ts"
    },
    {
      "path": "registry/widgets/controls/audio-widget.tsx",
      "content": "/**\n * Audio widget - plays audio from the kernel.\n *\n * Maps to ipywidgets AudioModel.\n */\n\nimport { useMemo } from \"react\";\nimport { Label } from \"@/components/ui/label\";\nimport { cn } from \"@/lib/utils\";\nimport { buildMediaSrc } from \"../buffer-utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport { useWidgetModelValue } from \"../widget-store-context\";\n\nexport function AudioWidget({ modelId, className }: WidgetComponentProps) {\n  const value = useWidgetModelValue<string | ArrayBuffer>(modelId, \"value\");\n  const format = useWidgetModelValue<string>(modelId, \"format\") ?? \"mp3\";\n  const autoplay = useWidgetModelValue<boolean>(modelId, \"autoplay\") ?? true;\n  const loop = useWidgetModelValue<boolean>(modelId, \"loop\") ?? true;\n  const controls = useWidgetModelValue<boolean>(modelId, \"controls\") ?? true;\n  const description = useWidgetModelValue<string>(modelId, \"description\");\n\n  const src = useMemo(\n    () => buildMediaSrc(value, \"audio\", format),\n    [value, format],\n  );\n\n  if (!value) {\n    return null;\n  }\n\n  return (\n    <div\n      className={cn(\"inline-flex items-start gap-3\", className)}\n      data-widget-id={modelId}\n      data-widget-type=\"Audio\"\n    >\n      {description && (\n        <Label className=\"shrink-0 pt-1 text-sm\">{description}</Label>\n      )}\n      {/* biome-ignore lint/a11y/useMediaCaption: ipywidgets audio does not provide captions */}\n      <audio src={src} autoPlay={autoplay} loop={loop} controls={controls} />\n    </div>\n  );\n}\n\nexport default AudioWidget;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/audio-widget.tsx"
    },
    {
      "path": "registry/widgets/controls/video-widget.tsx",
      "content": "/**\n * Video widget - plays video from the kernel.\n *\n * Maps to ipywidgets VideoModel.\n */\n\nimport { useMemo } from \"react\";\nimport { Label } from \"@/components/ui/label\";\nimport { cn } from \"@/lib/utils\";\nimport { buildMediaSrc } from \"../buffer-utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport { useWidgetModelValue } from \"../widget-store-context\";\n\nexport function VideoWidget({ modelId, className }: WidgetComponentProps) {\n  const value = useWidgetModelValue<string | ArrayBuffer>(modelId, \"value\");\n  const format = useWidgetModelValue<string>(modelId, \"format\") ?? \"mp4\";\n  const width = useWidgetModelValue<string>(modelId, \"width\") ?? \"\";\n  const height = useWidgetModelValue<string>(modelId, \"height\") ?? \"\";\n  const autoplay = useWidgetModelValue<boolean>(modelId, \"autoplay\") ?? true;\n  const loop = useWidgetModelValue<boolean>(modelId, \"loop\") ?? true;\n  const controls = useWidgetModelValue<boolean>(modelId, \"controls\") ?? true;\n  const description = useWidgetModelValue<string>(modelId, \"description\");\n\n  const src = useMemo(\n    () => buildMediaSrc(value, \"video\", format),\n    [value, format],\n  );\n\n  if (!value) {\n    return null;\n  }\n\n  const style: React.CSSProperties = {};\n  if (width) style.width = width;\n  if (height) style.height = height;\n\n  return (\n    <div\n      className={cn(\"inline-flex items-start gap-3\", className)}\n      data-widget-id={modelId}\n      data-widget-type=\"Video\"\n    >\n      {description && (\n        <Label className=\"shrink-0 pt-1 text-sm\">{description}</Label>\n      )}\n      {/* biome-ignore lint/a11y/useMediaCaption: ipywidgets video does not provide captions */}\n      <video\n        src={src}\n        autoPlay={autoplay}\n        loop={loop}\n        controls={controls}\n        style={style}\n        className=\"max-w-full\"\n      />\n    </div>\n  );\n}\n\nexport default VideoWidget;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/video-widget.tsx"
    },
    {
      "path": "registry/widgets/controls/ints-input-widget.tsx",
      "content": "\"use client\";\n\n/**\n * IntsInput widget - multi-value integer tag input.\n *\n * Maps to ipywidgets IntsInputModel.\n */\n\nimport { XIcon } from \"lucide-react\";\nimport { useCallback, useState } from \"react\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { cn } from \"@/lib/utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport {\n  useWidgetModelValue,\n  useWidgetStoreRequired,\n} from \"../widget-store-context\";\n\nexport function IntsInputWidget({ modelId, className }: WidgetComponentProps) {\n  const { sendUpdate } = useWidgetStoreRequired();\n\n  // Subscribe to individual state keys\n  const value = useWidgetModelValue<number[]>(modelId, \"value\") ?? [];\n  const allowDuplicates =\n    useWidgetModelValue<boolean>(modelId, \"allow_duplicates\") ?? true;\n  const placeholder =\n    useWidgetModelValue<string>(modelId, \"placeholder\") ?? \"Add integer...\";\n  const description = useWidgetModelValue<string>(modelId, \"description\");\n  const disabled = useWidgetModelValue<boolean>(modelId, \"disabled\") ?? false;\n  const min = useWidgetModelValue<number>(modelId, \"min\");\n  const max = useWidgetModelValue<number>(modelId, \"max\");\n\n  const [inputValue, setInputValue] = useState(\"\");\n\n  const handleKeyDown = useCallback(\n    (e: React.KeyboardEvent<HTMLInputElement>) => {\n      if (e.key === \"Enter\" && inputValue.trim()) {\n        e.preventDefault();\n        const parsed = parseInt(inputValue.trim(), 10);\n\n        // Validate integer\n        if (Number.isNaN(parsed)) return;\n\n        // Check bounds\n        if (min != null && parsed < min) return;\n        if (max != null && parsed > max) return;\n\n        // Check for duplicates\n        if (!allowDuplicates && value.includes(parsed)) return;\n\n        sendUpdate(modelId, { value: [...value, parsed] });\n        setInputValue(\"\");\n      } else if (e.key === \"Backspace\" && !inputValue && value.length > 0) {\n        sendUpdate(modelId, { value: value.slice(0, -1) });\n      }\n    },\n    [modelId, inputValue, value, allowDuplicates, min, max, sendUpdate],\n  );\n\n  const handleRemoveTag = useCallback(\n    (indexToRemove: number) => {\n      sendUpdate(modelId, {\n        value: value.filter((_, idx) => idx !== indexToRemove),\n      });\n    },\n    [modelId, value, sendUpdate],\n  );\n\n  return (\n    <div\n      className={cn(\"flex items-start gap-3\", className)}\n      data-widget-id={modelId}\n      data-widget-type=\"IntsInput\"\n    >\n      {description && (\n        <Label className=\"shrink-0 pt-2 text-sm\">{description}</Label>\n      )}\n      <div\n        className={cn(\n          \"flex min-h-10 flex-wrap items-center gap-1.5 rounded-md border border-input bg-background px-3 py-2\",\n          disabled && \"cursor-not-allowed opacity-50\",\n        )}\n      >\n        {value.map((num, idx) => (\n          <Badge\n            key={`${num}-${idx}`}\n            variant=\"secondary\"\n            className=\"gap-1 pr-1\"\n          >\n            {num}\n            {!disabled && (\n              <button\n                type=\"button\"\n                onClick={() => handleRemoveTag(idx)}\n                className=\"rounded-full p-0.5 hover:bg-muted\"\n              >\n                <XIcon className=\"size-3\" />\n              </button>\n            )}\n          </Badge>\n        ))}\n        <Input\n          type=\"text\"\n          inputMode=\"numeric\"\n          value={inputValue}\n          onChange={(e) => setInputValue(e.target.value)}\n          onKeyDown={handleKeyDown}\n          placeholder={value.length === 0 ? placeholder : \"\"}\n          disabled={disabled}\n          className=\"h-6 min-w-20 flex-1 border-0 p-0 shadow-none focus-visible:ring-0\"\n        />\n      </div>\n    </div>\n  );\n}\n\nexport default IntsInputWidget;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/ints-input-widget.tsx"
    },
    {
      "path": "registry/widgets/controls/floats-input-widget.tsx",
      "content": "\"use client\";\n\n/**\n * FloatsInput widget - multi-value float tag input.\n *\n * Maps to ipywidgets FloatsInputModel.\n */\n\nimport { XIcon } from \"lucide-react\";\nimport { useCallback, useState } from \"react\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { cn } from \"@/lib/utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport {\n  useWidgetModelValue,\n  useWidgetStoreRequired,\n} from \"../widget-store-context\";\n\nexport function FloatsInputWidget({\n  modelId,\n  className,\n}: WidgetComponentProps) {\n  const { sendUpdate } = useWidgetStoreRequired();\n\n  // Subscribe to individual state keys\n  const value = useWidgetModelValue<number[]>(modelId, \"value\") ?? [];\n  const allowDuplicates =\n    useWidgetModelValue<boolean>(modelId, \"allow_duplicates\") ?? true;\n  const placeholder =\n    useWidgetModelValue<string>(modelId, \"placeholder\") ?? \"Add number...\";\n  const description = useWidgetModelValue<string>(modelId, \"description\");\n  const disabled = useWidgetModelValue<boolean>(modelId, \"disabled\") ?? false;\n  const min = useWidgetModelValue<number>(modelId, \"min\");\n  const max = useWidgetModelValue<number>(modelId, \"max\");\n\n  const [inputValue, setInputValue] = useState(\"\");\n\n  const handleKeyDown = useCallback(\n    (e: React.KeyboardEvent<HTMLInputElement>) => {\n      if (e.key === \"Enter\" && inputValue.trim()) {\n        e.preventDefault();\n        const parsed = parseFloat(inputValue.trim());\n\n        // Validate float\n        if (Number.isNaN(parsed)) return;\n\n        // Check bounds\n        if (min != null && parsed < min) return;\n        if (max != null && parsed > max) return;\n\n        // Check for duplicates\n        if (!allowDuplicates && value.includes(parsed)) return;\n\n        sendUpdate(modelId, { value: [...value, parsed] });\n        setInputValue(\"\");\n      } else if (e.key === \"Backspace\" && !inputValue && value.length > 0) {\n        sendUpdate(modelId, { value: value.slice(0, -1) });\n      }\n    },\n    [modelId, inputValue, value, allowDuplicates, min, max, sendUpdate],\n  );\n\n  const handleRemoveTag = useCallback(\n    (indexToRemove: number) => {\n      sendUpdate(modelId, {\n        value: value.filter((_, idx) => idx !== indexToRemove),\n      });\n    },\n    [modelId, value, sendUpdate],\n  );\n\n  return (\n    <div\n      className={cn(\"flex items-start gap-3\", className)}\n      data-widget-id={modelId}\n      data-widget-type=\"FloatsInput\"\n    >\n      {description && (\n        <Label className=\"shrink-0 pt-2 text-sm\">{description}</Label>\n      )}\n      <div\n        className={cn(\n          \"flex min-h-10 flex-wrap items-center gap-1.5 rounded-md border border-input bg-background px-3 py-2\",\n          disabled && \"cursor-not-allowed opacity-50\",\n        )}\n      >\n        {value.map((num, idx) => (\n          <Badge\n            key={`${num}-${idx}`}\n            variant=\"secondary\"\n            className=\"gap-1 pr-1\"\n          >\n            {num}\n            {!disabled && (\n              <button\n                type=\"button\"\n                onClick={() => handleRemoveTag(idx)}\n                className=\"rounded-full p-0.5 hover:bg-muted\"\n              >\n                <XIcon className=\"size-3\" />\n              </button>\n            )}\n          </Badge>\n        ))}\n        <Input\n          type=\"text\"\n          inputMode=\"decimal\"\n          value={inputValue}\n          onChange={(e) => setInputValue(e.target.value)}\n          onKeyDown={handleKeyDown}\n          placeholder={value.length === 0 ? placeholder : \"\"}\n          disabled={disabled}\n          className=\"h-6 min-w-20 flex-1 border-0 p-0 shadow-none focus-visible:ring-0\"\n        />\n      </div>\n    </div>\n  );\n}\n\nexport default FloatsInputWidget;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/floats-input-widget.tsx"
    },
    {
      "path": "registry/widgets/controls/datetime-widget.tsx",
      "content": "\"use client\";\n\n/**\n * Datetime widget - renders a datetime-local input field.\n *\n * Maps to ipywidgets DatetimeModel (UTC/timezone-aware) and NaiveDatetimeModel (local time).\n *\n * DatetimeModel: Values are in UTC. We convert to local time for display and back to UTC on change.\n * NaiveDatetimeModel: Values are in local time. No conversion needed.\n */\n\nimport { useCallback } from \"react\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { cn } from \"@/lib/utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport {\n  useWidgetModel,\n  useWidgetModelValue,\n  useWidgetStoreRequired,\n} from \"../widget-store-context\";\n\ntype DatetimeValue =\n  | {\n      year: number;\n      month: number;\n      date: number;\n      hours: number;\n      minutes: number;\n      seconds: number;\n      milliseconds?: number;\n    }\n  | string\n  | null;\n\n/**\n * Create a Date from UTC components (for DatetimeModel).\n * The returned Date stores the time internally as UTC, so local getters will\n * automatically convert to the user's timezone.\n */\nfunction utcComponentsToDate(\n  value: Exclude<DatetimeValue, string | null>,\n): Date {\n  const date = new Date();\n  date.setUTCFullYear(value.year, value.month, value.date);\n  date.setUTCHours(\n    value.hours,\n    value.minutes,\n    value.seconds,\n    value.milliseconds ?? 0,\n  );\n  return date;\n}\n\n/**\n * Create a Date from local components (for NaiveDatetimeModel).\n */\nfunction localComponentsToDate(\n  value: Exclude<DatetimeValue, string | null>,\n): Date {\n  const date = new Date();\n  date.setFullYear(value.year, value.month, value.date);\n  date.setHours(\n    value.hours,\n    value.minutes,\n    value.seconds,\n    value.milliseconds ?? 0,\n  );\n  return date;\n}\n\n/**\n * Format a Date as datetime-local input string (YYYY-MM-DDTHH:MM).\n * Always uses local time getters since datetime-local displays in local time.\n */\nfunction formatDateForInput(date: Date): string {\n  const year = String(date.getFullYear()).padStart(4, \"0\");\n  const month = String(date.getMonth() + 1).padStart(2, \"0\");\n  const day = String(date.getDate()).padStart(2, \"0\");\n  const hours = String(date.getHours()).padStart(2, \"0\");\n  const minutes = String(date.getMinutes()).padStart(2, \"0\");\n  return `${year}-${month}-${day}T${hours}:${minutes}`;\n}\n\n/**\n * Convert datetime value to datetime-local input format (YYYY-MM-DDTHH:MM).\n * @param value - The datetime value from ipywidgets\n * @param isUtc - Whether the value is in UTC (DatetimeModel) or local time (NaiveDatetimeModel)\n */\nfunction toDatetimeLocalString(value: DatetimeValue, isUtc: boolean): string {\n  if (!value) return \"\";\n\n  if (typeof value === \"string\") {\n    const date = new Date(value);\n    if (Number.isNaN(date.getTime())) return \"\";\n    return formatDateForInput(date);\n  }\n\n  // Convert components to Date, then format for display\n  const date = isUtc\n    ? utcComponentsToDate(value)\n    : localComponentsToDate(value);\n  return formatDateForInput(date);\n}\n\n/**\n * Extract UTC components from a Date (for DatetimeModel).\n */\nfunction dateToUtcComponents(d: Date) {\n  return {\n    year: d.getUTCFullYear(),\n    month: d.getUTCMonth(),\n    date: d.getUTCDate(),\n    hours: d.getUTCHours(),\n    minutes: d.getUTCMinutes(),\n    seconds: d.getUTCSeconds(),\n    milliseconds: d.getUTCMilliseconds(),\n  };\n}\n\n/**\n * Extract local components from a Date (for NaiveDatetimeModel).\n */\nfunction dateToLocalComponents(d: Date) {\n  return {\n    year: d.getFullYear(),\n    month: d.getMonth(),\n    date: d.getDate(),\n    hours: d.getHours(),\n    minutes: d.getMinutes(),\n    seconds: d.getSeconds(),\n    milliseconds: d.getMilliseconds(),\n  };\n}\n\nexport function DatetimeWidget({ modelId, className }: WidgetComponentProps) {\n  const { sendUpdate } = useWidgetStoreRequired();\n  const model = useWidgetModel(modelId);\n\n  // Determine if this is a timezone-aware (UTC) model or naive (local) model\n  const isUtc = model?.modelName === \"DatetimeModel\";\n\n  // Subscribe to individual state keys\n  const value = useWidgetModelValue<DatetimeValue>(modelId, \"value\") ?? null;\n  const min = useWidgetModelValue<DatetimeValue>(modelId, \"min\") ?? null;\n  const max = useWidgetModelValue<DatetimeValue>(modelId, \"max\") ?? null;\n  const description = useWidgetModelValue<string>(modelId, \"description\");\n  const disabled = useWidgetModelValue<boolean>(modelId, \"disabled\") ?? false;\n\n  const handleChange = useCallback(\n    (e: React.ChangeEvent<HTMLInputElement>) => {\n      const newValue = e.target.value;\n      if (newValue) {\n        // datetime-local input gives us a local time string\n        const d = new Date(newValue);\n        // Convert to appropriate format based on model type\n        const components = isUtc\n          ? dateToUtcComponents(d)\n          : dateToLocalComponents(d);\n        sendUpdate(modelId, { value: components });\n      } else {\n        sendUpdate(modelId, { value: null });\n      }\n    },\n    [modelId, sendUpdate, isUtc],\n  );\n\n  return (\n    <div\n      className={cn(\"flex items-center gap-3\", className)}\n      data-widget-id={modelId}\n      data-widget-type=\"Datetime\"\n    >\n      {description && <Label className=\"shrink-0 text-sm\">{description}</Label>}\n      <Input\n        type=\"datetime-local\"\n        value={toDatetimeLocalString(value, isUtc)}\n        min={toDatetimeLocalString(min, isUtc)}\n        max={toDatetimeLocalString(max, isUtc)}\n        disabled={disabled}\n        onChange={handleChange}\n        className=\"w-52\"\n      />\n    </div>\n  );\n}\n\nexport default DatetimeWidget;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/datetime-widget.tsx"
    },
    {
      "path": "registry/widgets/controls/stack-widget.tsx",
      "content": "/**\n * Stack widget - single-child container showing one child at a time.\n *\n * Maps to ipywidgets StackModel. Like Tab but without the tab headers —\n * displays only the child at `selected_index`.\n */\n\nimport { cn } from \"@/lib/utils\";\nimport type { WidgetComponentProps } from \"../widget-registry\";\nimport { parseModelRef, useWidgetModelValue } from \"../widget-store-context\";\nimport { WidgetView } from \"../widget-view\";\n\nexport function StackWidget({ modelId, className }: WidgetComponentProps) {\n  // Subscribe to individual state keys\n  const children = useWidgetModelValue<string[]>(modelId, \"children\");\n  const selectedIndex =\n    useWidgetModelValue<number>(modelId, \"selected_index\") ?? 0;\n\n  if (!children || children.length === 0) return null;\n\n  const selectedRef = children[selectedIndex];\n  if (!selectedRef) return null;\n\n  const childId = parseModelRef(selectedRef);\n  if (!childId) return null;\n\n  return (\n    <div\n      className={cn(\"w-full\", className)}\n      data-widget-id={modelId}\n      data-widget-type=\"Stack\"\n    >\n      <WidgetView modelId={childId} />\n    </div>\n  );\n}\n\nexport default StackWidget;\n",
      "type": "registry:component",
      "target": "components/widgets/controls/stack-widget.tsx"
    }
  ],
  "type": "registry:component"
}